Apprenons à déployer des microservices. Partie 1. Spring Boot et Docker

Apprenons à déployer des microservices. Partie 1. Spring Boot et Docker

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 :

  1. 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

  2. Développement de la configuration Kubernetes et déploiement du systÚme sur Google Kubernetes Engine

    Mots-clés : Kubernetes, GKE, gestion des ressources, autoscaling, secrets

  3. Création d'un chart avec Helm 3 pour une gestion plus efficace du cluster

    Mots-clés : Helm 3, déploiement de chart

  4. 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 : backend et passerelle.

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.jar

Démarrage de la passerelle :

./mvnw package -DskipTests
java -jar target/microservices-gateway-1.0.0.jar

Vé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 (http://localhost:8081/requests). 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 Docker 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° DockerHub) 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.0

Nous 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. https://habr.com/ru/company/ruvds/blog/485650/.

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-network

Ensuite, 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.0

Il 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 http://backend:8080.

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.0

Dans 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 http://localhost/ 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 (un bon article).

Dans de la partie suivante je parlerai de la façon dont j'ai configuré Kubernetes et déployé le projet sur Google Kubernetes Engine.

Source : habr.com

Acheter un hĂ©bergement fiable pour les sites avec protection DDoS, serveurs VPS VDS đŸ”„ Acheter un hĂ©bergement fiable pour les sites avec protection DDoS, serveurs VPS VDS | ProHoster