Docker: comment déployer une application full-stack et ne pas devenir gris

"Nous avons besoin de DevOps!"
(la phrase la plus populaire Ă  la fin de tout hackathon)

Tout d'abord, quelques paroles.

Quand un développeur est un excellent développeur qui peut déployer son idée sur n'importe quelle machine sous n'importe quel OC, c'est un plus. Cependant, s'il ne comprend rien de plus que son IDE, ce n'est pas un inconvénient - au final, il est payé pour le code, et non pour la possibilité de le déployer. Un spécialiste en profondeur étroit sur le marché est évalué plus haut que la compétence moyenne de «cric de tous les métiers». Pour des gens comme nous, «utilisateurs IDE», de bonnes personnes ont proposé Docker.

Le principe de Docker est le suivant: "ça marche pour moi - ça marche partout". Docker est le seul programme nécessaire pour déployer une copie de votre application n'importe où. Si vous exécutez votre application dans le docker de votre machine, elle est garantie de fonctionner avec le même succès dans n'importe quel autre docker. Et rien d'autre qu'un docker doit être installé. Par exemple, je n'ai même pas Java sur le serveur virtuel.

Comment fonctionne Docker?


Docker crée une image d'une machine virtuelle avec des applications installées. De plus, cette image se déploie comme une machine virtuelle complètement autonome. Une copie en cours d'exécution de l'image est appelée un «conteneur». Vous pouvez exécuter un nombre illimité d'images sur le serveur et chacune d'entre elles sera une machine virtuelle distincte avec son propre environnement.

Qu'est-ce qu'une machine virtuelle? Il s'agit de l'emplacement encapsulé sur le serveur avec le système d'exploitation dans lequel les applications sont installées. Dans tout système d'exploitation, un grand nombre d'applications tournent généralement, dans le nôtre il y en a une.

Le schéma de déploiement de conteneur peut être représenté comme suit:



Pour chaque application, nous créons notre propre image, puis déployons chaque conteneur séparément. En outre, vous pouvez mettre toutes les applications dans une image et les déployer en un seul conteneur. De plus, afin de ne pas déployer chaque conteneur séparément, nous pouvons utiliser un utilitaire docker-compose séparé, qui configure les conteneurs et la relation entre eux via un fichier séparé. La structure de l'application entière peut alors ressembler à ceci:



Je n'ai pas intentionnellement contribué à la base de données à l'assemblage Docker général, pour plusieurs raisons. Premièrement, la base de données est complètement indépendante des applications qui fonctionnent avec elle. Cela peut être loin d'une application, il peut s'agir de requêtes manuelles depuis la console. Personnellement, je ne vois aucune raison de rendre la base de données dépendante de l'assemblage Docker dans lequel elle se trouve. Par conséquent, je l'ai enduré. Cependant, une approche est souvent pratiquée dans laquelle la base de données est placée dans une image distincte et lancée dans un conteneur séparé. Deuxièmement, je veux montrer comment le conteneur Docker interagit avec les systèmes en dehors du conteneur.

Cependant, tout à fait les paroles, écrivons le code. Nous écrirons l'application la plus simple au printemps et réagirons, qui enregistrera nos appels à l'avant dans la base de données, et nous soulèverons tout cela via Docker. La structure de notre application ressemblera à ceci:



Il existe de nombreuses façons de mettre en œuvre une telle structure. Nous mettons en œuvre l'un d'entre eux. Nous allons créer deux images, en lancer deux conteneurs, et le backend se connectera à la base de données installée sur un serveur spécifique quelque part sur Internet (oui, de telles requêtes de base de données n'iront pas rapidement, mais nous ne sommes pas motivés par la soif d'optimisation, mais intérêt scientifique).

Le poste sera divisé en plusieurs parties:

0. Installez Docker.
1. Nous rédigeons des candidatures.
2. Nous collectons des images et lançons des conteneurs.
3. Collectez des images et lancez des conteneurs sur un serveur distant.
4. Résolvez les problèmes de réseau.

0. Installez Docker


Pour installer Docker, vous devez vous rendre sur le site et suivre ce qui y est écrit. Lors de l'installation de Docker sur un serveur distant, préparez-vous au fait que Docker peut ne pas fonctionner avec des serveurs sur OpenVZ. De plus, il peut y avoir des problèmes si vous n'avez pas activé iptables. Il est conseillé de démarrer le serveur sur KVM avec iptables. Mais ce sont mes recommandations. Si tout fonctionne pour vous, et donc, je serai heureux que vous n'ayez pas passé beaucoup de temps à comprendre pourquoi cela ne fonctionne pas, comment je devais le faire.

1. Nous rédigeons des candidatures


Écrivons une application simple avec le backend le plus primitif sur Spring Boot, un frontend très simple sur ReactJS et une base de données MySQL. L'application aura une page unique avec un seul bouton qui enregistrera l'heure à laquelle il a été cliqué dessus dans la base de données.

J'espère que vous savez déjà comment écrire des applications au démarrage, mais sinon, vous pouvez cloner le projet terminé. Tous les liens à la fin de l'article.

Backend sur Spring Boot


build.gradle:

plugins { id 'org.springframework.boot' version '2.1.4.RELEASE' id 'java' } apply plugin: 'io.spring.dependency-management' group = 'ru.xpendence' version = '0.0.2' sourceCompatibility = '1.8' repositories { mavenCentral() } dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-web' runtimeOnly 'mysql:mysql-connector-java' testImplementation 'org.springframework.boot:spring-boot-starter-test' } 

Entité de journal:

 package ru.xpendence.rebounder.entity; import com.fasterxml.jackson.annotation.JsonFormat; import javax.persistence.*; import java.io.Serializable; import java.time.LocalDateTime; import java.util.Objects; /** * Author: Vyacheslav Chernyshov * Date: 14.04.19 * Time: 21:20 * e-mail: 2262288@gmail.com */ @Entity @Table(name = "request_logs") public class Log implements Serializable { private Long id; private LocalDateTime created; @Id @GeneratedValue(strategy = GenerationType.IDENTITY) public Long getId() { return id; } @Column @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss.SSS") public LocalDateTime getCreated() { return created; } @PrePersist public void prePersist() { this.created = LocalDateTime.now(); } //setters, toString, equals, hashcode, constructors 

LogController, qui fonctionne sur une logique simplifiée et écrit immédiatement dans la base de données. Nous omettons le service.

 package ru.xpendence.rebounder.controller; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import ru.xpendence.rebounder.entity.Log; import ru.xpendence.rebounder.repository.LogRepository; import java.util.logging.Logger; /** * Author: Vyacheslav Chernyshov * Date: 14.04.19 * Time: 22:24 * e-mail: 2262288@gmail.com */ @RestController @RequestMapping("/log") public class LogController { private final static Logger LOG = Logger.getLogger(LogController.class.getName()); private final LogRepository repository; @Autowired public LogController(LogRepository repository) { this.repository = repository; } @GetMapping public ResponseEntity<Log> log() { Log log = repository.save(new Log()); LOG.info("saved new log: " + log.toString()); return ResponseEntity.ok(log); } } 

Tout, comme nous le voyons, est très simple. Par une demande GET, nous écrivons dans la base de données et renvoyons le résultat.

Nous discuterons séparément du fichier de paramètres d'application. Il y en a deux.

application.yml:

 spring: profiles: active: remote 

application-remote.yml:

 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://my-remote-server-database:3306/rebounder_database?useUnicode=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC username: admin password: 12345 jpa: hibernate: ddl-auto: update show-sql: true properties: hibernate.dialect: org.hibernate.dialect.MySQL5Dialect server: port: 8099 

Comment cela fonctionne, vous savez probablement que Spring scanne d'abord le fichier application.properties ou application.yml - celui qu'il trouve. Nous y indiquons un seul paramètre - quel profil nous utiliserons. Habituellement, pendant le développement, j'accumule plusieurs profils, et il est très pratique de les changer en utilisant le profil par défaut. Ensuite, Spring trouve application.yml avec le suffixe souhaité et l'utilise.

Nous avons spécifié la source de données, les paramètres JPA et, surtout, le port externe de notre backend.

ReactJS Frontend


Vous pouvez également voir l'interface dans un projet sur git, ou vous ne pouvez même pas regarder, mais le cloner et l'exécuter.

Vous pouvez vérifier le travail individuel du frontend en téléchargeant le projet, en vous rendant dans le dossier racine du projet dans le terminal (où se trouve le fichier package.json) et en exécutant deux commandes en séquence:

 npm install //      ,  maven npm start //   

Bien sûr, pour cela, vous avez besoin du Node Package Manager (npm) installé, et c'est le moyen le plus difficile que nous évitons d'utiliser Docker. Si vous avez toujours démarré le projet, vous verrez la fenêtre suivante:



Eh bien, il est temps de regarder le code. Je n'indiquerai que la partie qui se réfère au backend.

 export default class Api { _apiPath = 'http://localhost:8099'; _logUrl = '/log'; getResource = async () => { const res = await fetch(`${this._apiPath}${this._logUrl}`); if (!res.ok) { throw new Error(`Could not fetch ${this._logUrl}` + `, received ${res.status}`) } return await res.json(); }; }; 

L'interface fonctionne de manière prévisible. Nous suivons le lien, attendons la réponse et l'affiche à l'écran.



Il convient de se concentrer sur les points suivants:

  1. La façade est ouverte sur le monde extérieur via le port 3000. Il s'agit du port par défaut pour React.
  2. L'arrière est ouvert sur le port 8099. Nous l'avons défini dans les paramètres de l'application.
  3. Le dos frappe à la base de données via Internet externe.

L'application est prĂŞte.

2. Collectez des images et lancez des conteneurs


La structure de notre assemblée sera la suivante. Nous allons créer deux images - frontend et backend, qui communiqueront entre elles via des ports externes. Pour la base, nous ne créerons pas d'image, nous l'installerons séparément. Pourquoi Pourquoi ne créons-nous pas une image pour la base? Nous avons deux applications qui changent constamment et ne stockent pas de données en nous-mêmes. La base de données stocke les données en soi, ce qui peut être le résultat de plusieurs mois de fonctionnement de l'application. De plus, cette base de données est accessible non seulement par notre application backend, mais aussi par bien d'autres - pour cela c'est aussi une base de données, et nous ne la réassemblerons pas en permanence. Encore une fois, c'est l'occasion de travailler avec une API externe, qui, bien sûr, est de se connecter à notre base de données.

Assemblage frontal


Pour exécuter chaque application (avant ou arrière), vous avez besoin d'une certaine séquence d'actions. Pour exécuter l'application sur React, nous devons procéder comme suit (à condition que nous ayons déjà Linux):

  1. Installez NodeJS.
  2. Copiez l'application dans un dossier spécifique.
  3. Installez les packages requis (commande npm install).
  4. Lancez l'application avec la commande npm start.

C'est cette séquence d'actions que nous devons effectuer dans le docker. Pour ce faire, à la racine du projet (au même endroit que se trouve package.json), nous devons placer le Dockerfile avec le contenu suivant:

 FROM node:alpine WORKDIR /usr/app/front EXPOSE 3000 COPY ./ ./ RUN npm install CMD ["npm", "start"] 

Voyons ce que signifie chaque ligne.

 FROM node:alpine 

Avec cette ligne, nous indiquons clairement au docker que lorsque vous démarrez le conteneur, la première chose que vous devez faire est de télécharger Docker à partir du référentiel et d'installer NodeJS, et la plus légère (toutes les versions les plus légères des frameworks et bibliothèques populaires dans docker sont généralement appelées alpines).

 WORKDIR /usr/app/front 

Dans le conteneur Linux, les mêmes dossiers standard seront créés comme dans les autres Linux - / opt, / home, / etc, / usr et ainsi de suite. Nous avons défini le répertoire de travail avec lequel nous travaillerons - / usr / app / front.

 EXPOSE 3000 

Nous ouvrons le port 3000. Une communication supplémentaire avec l'application exécutée dans le conteneur se fera via ce port.

 COPY ./ ./ 

Copiez le contenu du projet source dans le dossier de travail du conteneur.

 RUN npm install 

Installez tous les packages nécessaires pour exécuter l'application.

 CMD ["npm", "start"] 

Nous démarrons l'application avec la commande npm start.

Ce scénario sera exécuté dans notre application au démarrage du conteneur.

Mettons le front droit. Pour ce faire, dans le terminal, se trouvant dans le dossier racine du projet (où se trouve le Dockerfile), exécutez la commande:

 docker build -t rebounder-chain-frontend . 

Valeurs de commande:

docker est un appel Ă  l'application docker, eh bien, vous le savez.
build - construire une image à partir de matériaux cibles.
-t <nom> - à l'avenir, l'application sera disponible par la balise spécifiée ici. Vous pouvez omettre cela, puis Docker générera sa propre balise, mais il sera impossible de la distinguer des autres.
. - indique que vous devez collecter le projet Ă  partir du dossier actuel.



Par conséquent, l'assemblage doit se terminer par le texte:

 Step 7/7 : CMD ["npm", "start"] ---> Running in ee0e8a9066dc Removing intermediate container ee0e8a9066dc ---> b208c4184766 Successfully built b208c4184766 Successfully tagged rebounder-chain-frontend:latest 

Si nous voyons que la dernière étape est terminée et que tout est réussi, alors nous avons une image. Nous pouvons le vérifier en l'exécutant:

 docker run -p 8080:3000 rebounder-chain-frontend 

Je pense que la signification de cette commande est généralement comprise, à l'exception de l'entrée -p 8080: 3000.
docker run rebounder-chain-frontend - signifie que nous lançons une telle image de docker, que nous avons appelée rebounder-chain-frontend. Mais un tel conteneur n'aura pas de sortie vers l'extérieur, il doit définir un port. C'est l'équipe ci-dessous qui le définit. Nous nous souvenons que notre application React s'exécute sur le port 3000. La commande -p 8080: 3000 indique au docker de prendre le port 3000 et de le transmettre au port 8080 (qui sera ouvert). Ainsi, une application qui s'exécute sur le port 3000 sera ouverte sur le port 8080 et sera disponible sur la machine locale sur ce port.

 ,       : Mac-mini-Vaceslav:rebounder-chain-frontend xpendence$ docker run -p 8080:3000 rebounder-chain-frontend > rebounder-chain-frontend@0.1.0 start /usr/app/front > react-scripts start Starting the development server... Compiled successfully! You can now view rebounder-chain-frontend in the browser. Local: http://localhost:3000/ On Your Network: http://172.17.0.2:3000/ Note that the development build is not optimized. To create a production build, use npm run build. 

Ne laissez pas le dossier vous déranger

  Local: http://localhost:3000/ On Your Network: http://172.17.0.2:3000/ 

React le pense. Il est vraiment disponible dans le conteneur sur le port 3000, mais nous avons transmis ce port au port 8080, et à partir du conteneur, l'application s'exécute sur le port 8080. Vous pouvez exécuter l'application localement et vérifier cela.

Nous avons donc un conteneur prĂŞt Ă  l'emploi avec une application frontale, collectons maintenant le backend.

Construire un back-end.


Le script de lancement d'une application en Java est très différent de l'assemblage précédent. Il se compose des éléments suivants:

  1. Installez la JVM.
  2. Nous collectons des archives de pots.
  3. Nous le lançons.

Dans Dockerfile, ce processus ressemble Ă  ceci:

 # back #     JVM FROM openjdk:8-jdk-alpine #  . ,    .  . LABEL maintainer="2262288@gmail.com" #         (  ,  ) VOLUME /tmp #  ,        EXPOSE 8099 # ,       ARG JAR_FILE=build/libs/rebounder-chain-backend-0.0.2.jar #       rebounder-chain-backend.jar ADD ${JAR_FILE} rebounder-chain-backend.jar #    ENTRYPOINT ["java","-jar","/rebounder-chain-backend.jar"] 

Le processus d'assemblage d'une image avec l'inclusion d'un dzharnik sur certains points ressemble Ă  celui de notre front.

Le processus d'assemblage et de lancement de la deuxième image est essentiellement le même que l'assemblage et le lancement de la première.

 docker build -t rebounder-chain-backend . docker run -p 8099:8099 rebounder-chain-backend 

Maintenant, si vous avez les deux conteneurs en cours d'exécution et que le backend est connecté à la base de données, tout fonctionnera. Je vous rappelle que vous devez vous-même enregistrer la connexion à la base de données depuis le backend, et qu'elle doit fonctionner via un réseau externe.

3. Collectez des images et exécutez des conteneurs sur un serveur distant


Pour que tout fonctionne sur le serveur distant, nous avons besoin de Docker déjà installé dessus, après quoi il suffit d'exécuter les images. Nous irons dans le bon sens et engagerons nos images sur notre compte dans le cloud Docker, après quoi elles seront disponibles partout dans le monde. Bien sûr, il existe de nombreuses alternatives à cette approche, ainsi que tout ce qui est décrit dans le post, mais poussons-le un peu plus et faisons bien notre travail. Mauvais, comme l'a dit Andrei Mironov, nous avons toujours le temps de le faire.

Création d'un compte sur le hub Docker


La première chose que vous devez faire est d'obtenir un compte sur le hub Docker. Pour ce faire, accédez au hub et inscrivez-vous. Ce n'est pas difficile.

Ensuite, nous devons aller au terminal et nous connecter Ă  Docker.

 docker login 

Il vous sera demandé de saisir un nom d'utilisateur et un mot de passe. Si tout va bien, une notification apparaîtra dans le terminal que la connexion a réussi.

Validation d'images sur le Docker Hub


Ensuite, nous devons étiqueter nos images et les valider sur le hub. Ceci est fait par l'équipe selon le schéma suivant:

 docker tag   /_: 

Ainsi, nous devons spécifier le nom de notre image, login / repository et la balise sous laquelle notre image sera validée sur le hub.

Dans mon cas, cela ressemblait Ă  ceci:



Nous pouvons vérifier la présence de cette image dans le référentiel local à l'aide de la commande:

 Mac-mini-Vaceslav:rebounder-chain-backend xpendence$ docker image ls REPOSITORY TAG IMAGE ID CREATED SIZE xpendence/rebounder-chain-backend 0.0.2 c8f5b99e15a1 About an hour ago 141MB 

Notre image est prĂŞte Ă  s'engager. Commit:

 docker push xpendence/rebounder-chain-backend:0.0.2 

Un enregistrement de validation réussi doit apparaître.
Faites de mĂŞme avec le frontend:

 docker tag rebounder-chain-frontend xpendence/rebounder-chain-frontend:0.0.1 docker push xpendence/rebounder-chain-frontend:0.0.1 

Maintenant, si nous allons sur hub.docker.com, nous verrons deux images verrouillées. Qui sont disponibles de partout.





Félicitations Il nous suffit de passer à la dernière partie de notre travail: lancer des images sur un serveur distant.

Exécuter des images sur un serveur distant


Maintenant, nous pouvons exécuter notre image sur n'importe quelle machine avec Docker en remplissant une seule ligne dans le terminal (dans notre cas, nous devons exécuter séquentiellement deux lignes dans différents terminaux - une pour chaque image).

 docker run -p 8099:8099 xpendence/rebounder-chain-backend:0.0.2 docker run -p 8080:3000 xpendence/rebounder-chain-frontend:0.0.1 

Ce lancement a cependant un inconvénient. Lorsque le terminal est fermé, le processus se termine et l'application cesse de fonctionner. Pour éviter cela, nous pouvons exécuter l'application en mode «détaché»:

 docker run -d -p 8099:8099 xpendence/rebounder-chain-backend:0.0.2 docker run -d -p 8080:3000 xpendence/rebounder-chain-frontend:0.0.1 

Maintenant, l'application n'émettra pas de journal au terminal (cela peut, encore une fois, être configuré séparément), mais même lorsque le terminal est fermé, il ne s'arrêtera pas de fonctionner.

4. Résolution des problèmes de réseau


Si vous avez tout fait correctement, vous pouvez vous attendre à la plus grande déception en suivant ce post - il se pourrait bien que rien ne fonctionne. Par exemple, tout fonctionnait parfaitement pour vous et fonctionnait sur la machine locale (comme, par exemple, sur mon Mac), mais lorsqu'ils étaient déployés sur un serveur distant, les conteneurs cessaient de se voir (comme, par exemple, sur mon serveur distant sous Linux). Quel est le problème? Mais le problème est le suivant, et au début j'y ai fait allusion. Comme mentionné précédemment, lorsque le conteneur démarre, Docker crée une machine virtuelle distincte, y déploie Linux, puis installe l'application sur ce Linux. Cela signifie que l'hôte local conditionnel pour le conteneur en cours d'exécution est limité au conteneur lui-même et que l'application n'a pas connaissance de l'existence d'autres réseaux. Mais nous devons:

a) les conteneurs se sont vus.
b) le backend a vu la base de données.

Il existe deux solutions au problème.

1. Création d'un réseau interne.
2. Amener les conteneurs au niveau de l'hĂ´te.

1. Au niveau Docker, vous pouvez créer des réseaux, en outre, trois d'entre eux par défaut - pont , aucun et hôte .

Bridge est un réseau interne Docker isolé du réseau hôte. Vous pouvez accéder aux conteneurs uniquement via les ports que vous ouvrez lorsque le conteneur démarre avec la commande -p . Vous pouvez créer n'importe quel nombre de réseaux tels que des ponts .



Aucun n'est un réseau distinct pour un conteneur spécifique.

Hôte est le réseau hôte. Lorsque vous choisissez ce réseau, votre conteneur est entièrement accessible via l'hôte - la commande -p ne fonctionne tout simplement pas ici, et si vous avez déployé le conteneur sur ce réseau, vous n'avez pas besoin de spécifier un port externe - le conteneur est accessible par son port interne. Par exemple, si Dockerfile EXPOSE est défini sur 8090, c'est via ce port que l'application sera disponible.



Étant donné que nous devons avoir accès à la base de données du serveur, nous utiliserons cette dernière méthode et disposerons les conteneurs sur le réseau du serveur distant.

Cela se fait très simplement, nous supprimons la mention des ports de la commande de lancement de conteneur et spécifions le réseau hôte:

 docker run --net=host xpendence/rebounder-chain-frontend:0.0.8 

Connexion à la base que j'ai indiquée

 localhost:3306 

La connexion de l'avant à l'arrière devait être entièrement spécifiée, externe:

 http://<__:__> 

Si vous transférez le port interne vers le port externe, ce qui est souvent le cas pour les serveurs distants, vous devez spécifier le port interne pour la base de données et le port externe pour le conteneur.

Si vous voulez expérimenter avec des connexions, vous pouvez télécharger et construire un projet que j'ai spécialement écrit pour tester la connexion entre les conteneurs. Entrez simplement l'adresse souhaitée, appuyez sur Envoyer et en mode débogage, voyez ce qui a volé en arrière.

Le projet se trouve ici .

Conclusion


Il existe de nombreuses façons de créer et d'exécuter une image Docker. Pour ceux qui sont intéressés, je vous conseille d'apprendre le docker-compose. Ici, nous n'avons examiné qu'une des façons de travailler avec Docker. Bien sûr, cette approche au premier abord ne semble pas si simple. Mais voici un exemple - lors de la rédaction d'un article, j'avais des connexions sortantes sur un serveur distant. Et pendant le processus de débogage, j'ai dû modifier plusieurs fois les paramètres de connexion à la base de données. L'ensemble du montage et du déploiement s'inscrit dans mon ensemble de 4 lignes, après être entré dont j'ai vu le résultat sur un serveur distant. En mode de programmation extrême, Docker est indispensable.

Comme promis, je poste les sources d'application:

backend
frontend

Source: https://habr.com/ru/post/fr448094/


All Articles