Découvrez comment déployer des microservices. Partie 1. Spring Boot et Docker

Découvrez comment déployer des microservices. Partie 1. Spring Boot et Docker

Hé Habr.

Dans cet article, je veux parler de mon expérience dans la création d'un environnement d'apprentissage pour expérimenter les microservices. Quand j'ai appris chaque nouvel outil, j'ai toujours voulu l'essayer non seulement sur la machine locale, mais aussi dans des conditions plus réalistes. Par conséquent, j'ai décidé de créer une application de microservice simplifiée, qui pourra ensuite être "couverte" de toutes sortes de technologies intéressantes. La principale exigence du projet est sa proximité fonctionnelle maximale avec le système réel.

Au départ, j'ai découpé la création du projet en plusieurs étapes :

  1. Créez deux services - 'backend' (backend) et 'gateway' (passerelle), regroupez-les dans des images docker et configurez-les pour qu'ils fonctionnent ensemble

    Mots-clés : Java 11, Spring Boot, Docker, optimisation d'image

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

    Mots-clés : Kubernetes, GKE, gestion des ressources, mise à l'échelle automatique, secrets

  3. Création d'un graphique avec Helm 3 pour une meilleure gestion des clusters

    Balises : Helm 3, déploiement de cartes

  4. Configuration de Jenkins et du pipeline pour la livraison automatique du code au cluster

    Mots-clés : configuration Jenkins, plugins, référentiel de configurations séparé

Je prévois de consacrer un article séparé à chaque étape.

L'objectif de cette série d'articles n'est pas de savoir comment écrire des microservices, mais comment les faire fonctionner dans un système unique. Bien que toutes ces choses soient généralement en dehors de la responsabilité du développeur, je pense qu'il est toujours utile d'en connaître au moins 20% (ce qui, comme vous le savez, donne 80% du résultat). Certains sujets inconditionnellement importants, tels que la sécurité, seront laissés de côté dans ce projet, car l'auteur comprend peu de choses sur ce système créé uniquement pour un usage personnel. Je suis preneur de tout avis et critique constructive.

Création de microservices

Les services ont été écrits en Java 11 à l'aide de Spring Boot. L'interaction interservices est organisée à l'aide de REST. Le projet inclura un nombre minimum de tests (pour que plus tard il y ait quelque chose à tester dans Jenkins). Le code source des services est disponible sur GitHub : backend и passerelle.

Pour pouvoir vérifier l'état de chacun des services, un actionneur à ressort a été ajouté à leurs dépendances. Il créera un point de terminaison /actuator/health et renverra un statut 200 si le service est prêt à accepter le trafic, ou 504 s'il y a un problème. Dans ce cas, il s'agit d'un contrôle plutôt fictif, car les services sont très simples, et en cas de force majeure, ils ont plus de chances de devenir totalement indisponibles que de rester partiellement opérationnels. Mais dans les systèmes réels, Actuator peut aider à diagnostiquer un problème avant que les utilisateurs ne commencent à se battre. Par exemple, s'il y a des problèmes d'accès à la base de données, nous pouvons y répondre automatiquement en arrêtant de traiter les demandes avec une instance de service cassée.

Service d'arrière-plan

Le service backend comptera et renverra simplement le nombre de requêtes acceptées.

Code 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"));
    }
}

Passerelle de services

La passerelle transmettra la requête au service backend, en la complétant avec les informations suivantes :

  • identifiant de la passerelle. Il est nécessaire pour qu'il soit possible de distinguer une instance de la passerelle d'une autre par la réponse du serveur
  • Un "secret" qui jouera le rôle d'un mot de passe très important (numéro de la clé de cryptage d'un cookie important)

Configuration dans application.properties :

backend.url=http://localhost:8081
instance.id=${random.int}
secret="default-secret"

Adaptateur principal :

@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<String> response = restTemplate.getForEntity(
backendUrl + REQUESTS_ENDPOINT, String.class);
        return response.getBody();
    }
}

Manette:

@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("Number of requests %s (gateway %d, secret %s)", backendAdapter.getRequests(), instanceId, secret);
    }
}

Lancer:

Nous commençons le 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érifier:

$ curl http://localhost:8080/
Number of requests 1 (gateway 38560358, secret "default-secret")

Tout fonctionne. Un lecteur attentif notera que rien ne nous empêche d'accéder directement au backend, en contournant la passerelle (http://localhost:8081/requests). Pour résoudre ce problème, les services doivent être combinés en un seul réseau, et seule la passerelle doit "sortir" à l'extérieur.
De plus, les deux services partagent un système de fichiers, produisent des flux et peuvent à un moment donné commencer à interférer l'un avec l'autre. Ce serait bien d'isoler nos microservices. Cela peut être réalisé en distribuant des applications sur différentes machines (beaucoup d'argent, difficile), en utilisant des machines virtuelles (consommation de ressources, démarrage long) ou en utilisant la conteneurisation. Comme prévu, nous choisissons la troisième option et Docker comme outil de conteneurisation.

Docker

En bref, docker crée des conteneurs isolés, un par application. Pour utiliser docker, vous devez écrire un Dockerfile - des instructions pour créer et exécuter l'application. Ensuite, vous pouvez créer l'image, la télécharger dans le registre d'images (No. Docker Hub) et déployez votre microservice dans n'importe quel environnement dockerisé en une seule commande.

Dockerfile

L'une des caractéristiques les plus importantes d'une image est sa taille. Une image compacte se téléchargera plus rapidement à partir d'un référentiel distant, occupera moins d'espace et votre service démarrera plus rapidement. Toute image est construite sur la base de l'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 des packages minimaux.

Pour commencer, essayons d'écrire un Dockerfile "sur le front" (je dirai tout de suite que c'est un mauvais moyen, ne le faites pas) :

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 le JDK déjà installé pour construire notre projet. Avec la commande ADD, nous ajoutons le répertoire src actuel à l'image, le marquons comme fonctionnant (WORKDIR) et commençons la construction. La commande EXPOSE 8080 signale à docker que l'application dans le conteneur utilisera son port 8080 (cela ne rendra pas l'application accessible de l'extérieur, mais permettra d'accéder à l'application, par exemple, depuis un autre conteneur sur le même réseau docker ).

Pour regrouper des services dans des images, vous devez exécuter des commandes à partir de la racine de chaque projet :

docker image build . -t msvc-backend:1.0.0

Le résultat est une image de 456 Mo (dont l'image JDK de base occupait 340 Mo). Et tout cela malgré le fait que les classes de notre projet se comptent sur un doigt. Pour réduire la taille de notre image :

  • Nous utilisons un assemblage en plusieurs étapes. Dans la première étape, nous allons construire le projet, dans la deuxième étape, nous installerons le JRE, et dans la troisième étape, nous copierons le tout dans une nouvelle image Alpine propre. Au total, seuls les composants nécessaires seront dans l'image finale.
  • Utilisons la modularisation de java. À partir de Java 9, vous pouvez utiliser l'outil jlink pour créer un JRE à partir des seuls modules dont vous avez besoin

Pour les curieux, voici un bon article sur les approches de réduction d'image. https://habr.com/ru/company/ruvds/blog/485650/.

Fichier Docker 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 [email protected]"
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, par conséquent, elle a perdu 6 fois son poids, soit 77 Mo. Pas mal. Après cela, des images prêtes à l'emploi peuvent être téléchargées dans le registre d'images afin que vos images puissent être téléchargées sur Internet.

Services de co-exécution dans Docker

Pour commencer, nos services doivent être sur le même réseau. Il existe plusieurs types de réseaux dans Docker, et nous utilisons le plus primitif d'entre eux - le pont, qui vous permet de mettre en réseau des conteneurs fonctionnant sur le même hôte. Créez un réseau avec la commande suivante :

docker network create msvc-network

Ensuite, démarrez 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 de pont fournit une découverte de service prête à l'emploi pour les conteneurs par leurs noms. Autrement dit, le service backend sera disponible à l'intérieur du réseau docker à 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 indiquons que nous transférons le port 80 de notre hôte vers le port 8080 du conteneur. Nous utilisons les options env pour définir des variables d'environnement qui seront automatiquement lues par spring et remplacer les propriétés de application.properties.

Après le démarrage, nous appelons http://localhost/ et assurez-vous que tout fonctionne, comme dans le cas précédent.

Conclusion

En conséquence, nous avons créé deux microservices simples, les avons emballés dans des conteneurs Docker et les avons lancés ensemble sur la même machine. Le système qui en résulte présente cependant un certain nombre d'inconvénients :

  • Mauvaise tolérance aux pannes - tout fonctionne pour nous sur un seul serveur
  • Évolutivité médiocre - lorsque la charge augmente, il serait bien de déployer automatiquement des instances de service supplémentaires et d'équilibrer la charge entre elles
  • La complexité du lancement - nous devions entrer au moins 3 commandes, et avec certains paramètres (ce n'est que pour 2 services)

Pour résoudre les problèmes ci-dessus, il existe un certain nombre de solutions telles que Docker Swarm, Nomad, Kubernetes ou OpenShift. Si tout le système est écrit en Java, vous pouvez vous tourner vers Spring Cloud (bon article).

В partie suivante Je vais parler de la façon dont j'ai configuré Kubernetes et déployé le projet sur Google Kubernetes Engine.

Source: habr.com

Ajouter un commentaire