
Bonjour, Habr.
Dans cet article, je souhaite partager mon expérience dans la création d'un environnement d'apprentissage pour expérimenter avec les microservices. En étudiant chaque nouvel outil, j'ai toujours voulu l'essayer non seulement sur ma machine locale, mais aussi dans des conditions plus réalistes. C'est pourquoi j'ai décidé de créer une application microservices simplifiée, que je pourrai ensuite "agrémenter" de diverses technologies intéressantes. L'exigence principale du projet est sa plus grande proximité fonctionnelle avec un systÚme réel.
Au départ, j'ai divisé la création du projet en plusieurs étapes :
CrĂ©er deux services : un âbackendâ et une âpasserelleâ (gateway), les empaqueter en images Docker et configurer leur coopĂ©ration
Mots-clés : Java 11, Spring Boot, Docker, optimisation d'image
Mots-clés : Kubernetes, GKE, gestion des ressources, autoscaling, secrets
Création d'un chart avec Helm 3 pour une gestion plus efficace du cluster
Mots-clés : Helm 3, déploiement de chart
Configuration de Jenkins et création d'un pipeline pour une livraison automatique du code dans le cluster
Mots-clés : configuration Jenkins, plugins, dépÎt de configurations séparées
Je prévois de consacrer un article à chaque étape.
L'orientation de ce cycle d'articles ne consiste pas Ă expliquer comment Ă©crire des microservices, mais Ă faire en sorte qu'ils fonctionnent dans un systĂšme unifiĂ©. Bien que toutes ces choses soient gĂ©nĂ©ralement en dehors des responsabilitĂ©s du dĂ©veloppeur, je pense qu'il est tout de mĂȘme utile de les connaĂźtre au moins Ă 20% (qui, comme on le sait, apportent 80% du rĂ©sultat). Certains sujets indĂ©niablement importants, comme la sĂ©curitĂ©, seront laissĂ©s de cĂŽtĂ© dans ce projet, car l'auteur n'y comprend que peu de choses. Le systĂšme est conçu uniquement pour un usage personnel. Je suis ouvert Ă toute opinion et critique constructive.
Création de microservices
Les services ont été écrits en Java 11 avec Spring Boot. L'interaction entre services est organisée à l'aide de REST. Le projet comprendra un nombre minimal de tests (afin d'avoir quelque chose à tester dans Jenkins). Le code source des services est disponible sur GitHub : et .
Pour vĂ©rifier l'Ă©tat de chaque service, Spring Actuator a Ă©tĂ© ajoutĂ© Ă leurs dĂ©pendances. Il crĂ©era un endpoint /actuator/health et retournera le statut 200 si le service est prĂȘt Ă recevoir du trafic, ou 504 en cas de problĂšme. Dans ce cas, c'est une vĂ©rification plutĂŽt fictive, car les services sont trĂšs simples, et en cas de coup dur, ils deviendront probablement complĂštement inaccessibles plutĂŽt que de conserver une partie de leur fonctionnalitĂ©. Mais dans des systĂšmes rĂ©els, Actuator peut aider Ă diagnostiquer un problĂšme avant que les utilisateurs ne commencent Ă se heurter Ă celui-ci. Par exemple, en cas de problĂšmes d'accĂšs Ă la base de donnĂ©es, nous pourrons rĂ©agir automatiquement Ă cela en arrĂȘtant de traiter les requĂȘtes avec une instance dĂ©fectueuse du service.
Service Backend
Le service backend comptera simplement et renverra le nombre de requĂȘtes reçues.
Code du contrĂŽleur :
@RestController
public class RequestsCounterController {
private final AtomicLong counter = new AtomicLong();
@GetMapping("/requests")
public Long getRequestsCount() {
return counter.incrementAndGet();
}
}Test du contrĂŽleur :
@WebMvcTest(RequestsCounterController.class)
public class RequestsCounterControllerTests {
@Autowired
private MockMvc mockMvc;
@Test
public void firstRequest_one() throws Exception {
mockMvc.perform(get("/requests"))
.andExpect(status().isOk())
.andExpect(MockMvcResultMatchers.content().string("1"));
}
}Service Gateway
La passerelle redirigera la requĂȘte vers le service backend, en l'enrichissant d'informations supplĂ©mentaires :
- id de la passerelle. Cela permet de distinguer une instance de passerelle d'une autre par la réponse du serveur.
- Une "clé secrÚte", qui agira comme un mot de passe essentiel (n° de clé de chiffrement d'un cookie important).
Configuration dans application.properties :
backend.url=http://localhost:8081
instance.id=${random.int}
secret="default-secret"Adaptateur pour la communication avec le backend :
@Service
public class BackendAdapter {
private static final String REQUESTS_ENDPOINT = "/requests";
private final RestTemplate restTemplate;
@Value("${backend.url}")
private String backendUrl;
public BackendAdapter(RestTemplateBuilder builder) {
restTemplate = builder.build();
}
public String getRequests() {
ResponseEntity response = restTemplate.getForEntity(
backendUrl + REQUESTS_ENDPOINT, String.class);
return response.getBody();
}
}ContrĂŽleur :
@RestController
@RequiredArgsConstructor
public class EndpointController {
private final BackendAdapter backendAdapter;
@Value("${instance.id}")
private int instanceId;
@Value("${secret}")
private String secret;
@GetMapping("/")
public String getRequestsCount() {
return String.format("Nombre de requĂȘtes %s (passerelle %d, clĂ© secrĂšte %s)", backendAdapter.getRequests(), instanceId, secret);
}
}Lancement :
Démarrage du backend :
./mvnw package -DskipTests
java -Dserver.port=8081 -jar target/microservices-backend-1.0.0.jarDémarrage de la passerelle :
./mvnw package -DskipTests
java -jar target/microservices-gateway-1.0.0.jarVérifions :
$ curl http://localhost:8080/
Nombre de requĂȘtes 1 (passerelle 38560358, secret "default-secret")Tout fonctionne. L'observateur attentif remarquera qu'il n'y a rien qui nous empĂȘche d'accĂ©der directement au backend en contournant la passerelle (). Pour corriger cela, les services doivent ĂȘtre regroupĂ©s dans un seul rĂ©seau, et seule la passerelle doit ĂȘtre exposĂ©e Ă l'extĂ©rieur.
De plus, les deux services partagent un mĂȘme systĂšme de fichiers, gĂ©nĂ©rant des flux qui peuvent commencer Ă interfĂ©rer l'un avec l'autre Ă un moment donnĂ©. Il serait souhaitable d'isoler nos microservices. Cela peut ĂȘtre rĂ©alisĂ© en rĂ©partissant les applications sur diffĂ©rentes machines (coĂ»teux, compliquĂ©), en utilisant des machines virtuelles (exigeant en ressources, dĂ©marrage lent) ou en utilisant la conteneurisation. Comme prĂ©vu, nous choisissons la troisiĂšme option et comme outil de conteneurisation.
Docker
En rĂ©sumĂ©, Docker crĂ©e des conteneurs isolĂ©s, un pour chaque application. Pour utiliser Docker, il est nĂ©cessaire d'Ă©crire un Dockerfile â une instruction pour construire et exĂ©cuter l'application. Ensuite, vous pouvez crĂ©er une image, la tĂ©lĂ©charger dans un registre d'images (n° ) et dĂ©ployer votre microservice dans n'importe quel environnement conteneurisĂ© d'un seul coup.
Dockerfile
Une des caractĂ©ristiques les plus importantes de l'image est sa taille. Une image compacte se tĂ©lĂ©chargera plus rapidement depuis le dĂ©pĂŽt distant, prendra moins de place, et votre service dĂ©marrera plus rapidement. Chaque image est construite sur la base d'une image de base, et il est recommandĂ© de choisir l'option la plus minimaliste. Une bonne option est Alpine â une distribution Linux complĂšte avec un minimum de paquets.
Pour commencer, essayons d'écrire un Dockerfile "à la va-vite" (je précise que c'est une mauvaise approche, ne faites pas ça) :
FROM adoptopenjdk/openjdk11:jdk-11.0.5_10-alpine
ADD . /src
WORKDIR /src
RUN ./mvnw package -DskipTests
EXPOSE 8080
ENTRYPOINT ["java","-jar","target/microservices-gateway-1.0.0.jar"]Ici, nous utilisons une image de base basĂ©e sur Alpine avec JDK dĂ©jĂ installĂ© pour construire notre projet. Avec la commande ADD, nous ajoutons le rĂ©pertoire actuel src Ă l'image, le dĂ©signons comme rĂ©pertoire de travail (WORKDIR) et lançons la construction. La commande EXPOSE 8080 indique Ă Docker que l'application dans le conteneur utilisera son port 8080 (cela ne rend pas l'application accessible de l'extĂ©rieur, mais permet d'accĂ©der Ă l'application depuis un autre conteneur dans le mĂȘme rĂ©seau Docker).
Pour emballer les services dans des images, il faut exécuter les commandes depuis la racine de chaque projet :
docker image build . -t msvc-backend:1.0.0Nous obtenons donc une image de 456 Mo (dont l'image de base JDK 340 a occupé Mo). Et tout cela alors que le nombre de classes dans notre projet se compte sur les doigts de la main. Pour réduire la taille de notre image :
- Utilisons une construction multi-étapes. Dans la premiÚre étape, nous allons construire le projet, dans la seconde, nous installerons JRE, et dans la troisiÚme étape, nous copierons tout cela dans une nouvelle image Alpine propre. Au final, seule les composants nécessaires seront présents dans l'image finale.
- Nous allons utiliser la modularisation de Java. Depuis Java 9, il est possible de créer un JRE uniquement à partir des modules requis à l'aide de l'outil jlink.
Pour les curieux, voici un bon article sur les approches pour réduire la taille des images. .
Dockerfile final :
FROM adoptopenjdk/openjdk11:jdk-11.0.5_10-alpine as builder
ADD . /src
WORKDIR /src
RUN ./mvnw package -DskipTests
FROM alpine:3.10.3 as packager
RUN apk --no-cache add openjdk11-jdk openjdk11-jmods
ENV JAVA_MINIMAL="/opt/java-minimal"
RUN /usr/lib/jvm/java-11-openjdk/bin/jlink
--verbose
--add-modules
java.base,java.sql,java.naming,java.desktop,java.management,java.security.jgss,java.instrument
--compress 2 --strip-debug --no-header-files --no-man-pages
--release-info="add:IMPLEMENTOR=radistao:IMPLEMENTOR_VERSION=radistao_JRE"
--output "$JAVA_MINIMAL"
FROM alpine:3.10.3
LABEL maintainer="Anton Shelenkov anshelen@yandex.ru"
ENV JAVA_HOME=/opt/java-minimal
ENV PATH="$PATH:$JAVA_HOME/bin"
COPY --from=packager "$JAVA_HOME" "$JAVA_HOME"
COPY --from=builder /src/target/microservices-backend-*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java","-jar","/app.jar"]Nous recrĂ©ons l'image, et elle a finalement Ă©tĂ© rĂ©duite de 6 fois, ne faisant plus que 77 Mo. Pas mal. Ensuite, les images prĂȘtes peuvent ĂȘtre tĂ©lĂ©chargĂ©es dans le registre d'images, afin que vos images soient accessibles pour tĂ©lĂ©chargement depuis Internet.
Exécution conjointe de services dans Docker.
Tout d'abord, nos services doivent ĂȘtre sur le mĂȘme rĂ©seau. Il existe plusieurs types de rĂ©seaux dans Docker, et nous allons utiliser le plus basique d'entre eux â le bridge, qui permet de connecter des conteneurs exĂ©cutĂ©s sur le mĂȘme hĂŽte. CrĂ©ons un rĂ©seau avec la commande suivante :
docker network create msvc-networkEnsuite, lançons le conteneur backend nommĂ© âbackendâ avec l'image microservices-backend:1.0.0 :
docker run -dit --name backend --network msvc-net microservices-backend:1.0.0Il convient de noter que le réseau bridge offre par défaut la découverte de services pour les conteneurs par leur nom. Cela signifie que le service backend sera accessible à l'intérieur du réseau Docker à l'adresse .
Démarrage de la passerelle :
docker run -dit -p 80:8080 --env secret=my-real-secret --env BACKEND_URL=http://backend:8080/ --name gateway --network msvc-net microservices-gateway:1.0.0Dans cette commande, nous spécifions que nous redirigeons le port 80 de notre hÎte vers le port 8080 du conteneur. Nous utilisons l'option env pour définir des variables d'environnement qui seront automatiquement lues par Spring et remplaceront les propriétés de application.properties.
AprÚs le démarrage, nous appelons et nous assurons que tout fonctionne comme la derniÚre fois.
Conclusion
Au final, nous avons créé deux microservices simples, les avons emballés dans des conteneurs Docker et les avons exécutés ensemble sur une seule machine. Cependant, le systÚme résultant présente plusieurs inconvénients :
- Mauvaise tolĂ©rance aux pannes â tout fonctionne sur un seul serveur.
- Mauvaise Ă©volutivitĂ© â en cas d'augmentation de la charge, il serait souhaitable de dĂ©ployer automatiquement des instances supplĂ©mentaires des services et de rĂ©partir la charge entre elles.
- ComplexitĂ© de dĂ©marrage â nous avons dĂ» entrer au moins 3 commandes, avec des paramĂštres spĂ©cifiques (et cela uniquement pour 2 services).
Pour résoudre les problÚmes énumérés ci-dessus, il existe plusieurs solutions telles que Docker Swarm, Nomad, Kubernetes ou OpenShift. Si tout le systÚme est écrit en Java, il vaut la peine de se tourner vers Spring Cloud ().
Dans je parlerai de la façon dont j'ai configuré Kubernetes et déployé le projet sur Google Kubernetes Engine.
Source : habr.com
