Docker: como implantar um aplicativo de pilha completa e não ficar cinza

"Precisamos de DevOps!"
(a frase mais popular no final de qualquer hackathon)

Primeiro, algumas letras.

Quando um desenvolvedor é um excelente desenvolvedor que pode implantar sua ideia em qualquer máquina sob qualquer OC, isso é uma vantagem. No entanto, se ele não entende nada além de seu IDE, isso não é um sinal de menos - no final, ele é pago pelo código, não pela capacidade de implementá-lo. Um especialista estreito e profundo no mercado é avaliado acima da habilidade média de "valete de todos os negócios". Para pessoas como nós, “usuários de IDE”, pessoas boas criaram o Docker.

O princípio do Docker é o seguinte: "funciona para mim - funciona em todos os lugares". O único programa necessário para implantar uma cópia do seu aplicativo em qualquer lugar é o Docker. Se você executar seu aplicativo na janela de encaixe na sua máquina, é garantido que ele será executado com o mesmo sucesso em qualquer outra janela de encaixe. E nada além de uma janela de encaixe precisa ser instalado. Por exemplo, eu nem tenho Java no servidor virtual.

Como o docker funciona?


O Docker cria uma imagem de uma máquina virtual com aplicativos instalados nela. Além disso, essa imagem se desdobra como uma máquina virtual completamente autônoma. Uma cópia em execução da imagem é chamada de "contêiner". Você pode executar qualquer número de imagens no servidor e cada uma delas será uma máquina virtual separada com seu próprio ambiente.

O que é uma máquina virtual? Este é o local encapsulado no servidor com o sistema operacional no qual os aplicativos estão instalados. Em qualquer sistema operacional, um grande número de aplicativos geralmente está girando, no nosso existe um.

O esquema de implantação de contêiner pode ser representado da seguinte maneira:



Para cada aplicativo, criamos nossa própria imagem e implantamos cada contêiner separadamente. Além disso, você pode colocar todos os aplicativos em uma imagem e implantar como um contêiner. Além disso, para não implantar cada contêiner separadamente, podemos usar um utilitário docker-compose separado que configura os contêineres e o relacionamento entre eles por meio de um arquivo separado. Em seguida, a estrutura de todo o aplicativo pode ficar assim:



Intencionalmente, não contribuí com o banco de dados para a assembléia geral do Docker, por vários motivos. Primeiro, o banco de dados é completamente independente dos aplicativos que trabalham com ele. Pode estar longe de um aplicativo, pode ser solicitações manuais do console. Pessoalmente, não vejo razão para tornar o banco de dados dependente do assembly do Docker no qual está localizado. Portanto, eu aguentei. No entanto, muitas vezes é praticada uma abordagem na qual o banco de dados é colocado em uma imagem separada e iniciado em um contêiner separado. Em segundo lugar, quero mostrar como o contêiner do Docker interage com os sistemas externos ao contêiner.

No entanto, bem as letras, vamos escrever o código. Escreveremos o aplicativo mais simples na primavera e reagiremos, que gravará nossas chamadas para a frente no banco de dados e elevaremos tudo isso através do Docker. A estrutura do nosso aplicativo ficará assim:



Existem muitas maneiras de implementar essa estrutura. Estamos implementando um deles. Criaremos duas imagens, lançaremos dois contêineres a partir deles, além disso, o back-end se conectará ao banco de dados, instalado em um servidor específico em algum lugar da Internet (sim, essas solicitações ao banco de dados não serão rápidas, mas não somos motivados pela necessidade de otimização, mas interesse científico).

A postagem será dividida em partes:

0. Instale o Docker.
1. Nós escrevemos aplicativos.
2. Coletamos imagens e lançamos contêineres.
3. Colete imagens e inicie contêineres em um servidor remoto.
4. Resolva os problemas de rede.

0. Instale o Docker


Para instalar o Docker, você precisa ir ao site e seguir o que está escrito lá. Ao instalar o Docker em um servidor remoto, esteja preparado para o fato de o Docker não funcionar com servidores no OpenVZ. Além disso, pode haver problemas se você não tiver o iptables ativado. É aconselhável iniciar o servidor no KVM com o iptables. Mas estas são minhas recomendações. Se tudo funcionar para você, e assim, ficarei feliz que você não gastou muito tempo descobrindo por que não funciona, como eu tive que fazê-lo.

1. Nós escrevemos aplicativos


Vamos escrever um aplicativo simples com o backend mais primitivo no Spring Boot, um frontend muito simples no ReactJS e um banco de dados MySQL. O aplicativo terá uma página única com um único botão que registrará a hora em que foi clicada no banco de dados.

Confio no fato de que você já sabe escrever aplicativos na inicialização, mas, se não, pode clonar o projeto finalizado. Todos os links no final do artigo.

Back-end na Inicialização Spring


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' } 

Entidade de log:

 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, que funcionará na lógica simplificada e gravará imediatamente no banco de dados. Nós omitimos o serviço.

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

Tudo, como vemos, é muito simples. Por uma solicitação GET, escrevemos no banco de dados e retornamos o resultado.

Discutiremos o arquivo de configurações do aplicativo separadamente. Existem dois deles.

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 

Como isso funciona, você provavelmente sabe que o Spring verifica primeiro o arquivo application.properties ou application.yml - qual ele encontra. Nele, indicamos uma única configuração - qual perfil usaremos. Normalmente, durante o desenvolvimento, acumulo vários perfis, e é muito conveniente alterná-los usando o perfil padrão. Em seguida, o Spring localiza application.yml com o sufixo desejado e o utiliza.

Especificamos a fonte de dados, as configurações JPA e, principalmente, a porta externa do nosso back-end.

ReactJS Frontend


Você também pode ver o frontend em um projeto no git, ou até não pode assistir, mas cloná-lo e executá-lo.

Você pode verificar o trabalho individual do front-end fazendo o download do projeto, acessando a pasta raiz do projeto no terminal (onde o arquivo package.json está localizado) e executando dois comandos em sequência:

 npm install //      ,  maven npm start //   

Obviamente, para isso, você precisa do Node Package Manager (npm) instalado, e é a maneira mais difícil de evitar o uso do Docker. Se você ainda iniciou o projeto, verá a seguinte janela:



Oh, bem, é hora de olhar para o código. Vou indicar apenas a parte que se refere ao back-end.

 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(); }; }; 

O frontend funciona previsivelmente. Seguimos o link, aguardamos a resposta e a exibimos na tela.



Vale a pena focar nos seguintes pontos:

  1. A frente está aberta para o mundo exterior pela porta 3000. Essa é a porta padrão para o React.
  2. A parte traseira é aberta na porta 8099. Definimos nas configurações do aplicativo.
  3. A parte de trás está batendo no banco de dados através da Internet externa.

O aplicativo está pronto.

2. Colete imagens e lance contêineres


A estrutura da nossa montagem será a seguinte. Criaremos duas imagens - front-end e back-end, que se comunicarão através de portas externas. Para a base, não criaremos uma imagem, instalaremos separadamente. Porque Por que não criamos uma imagem para a base? Temos dois aplicativos que mudam constantemente e não armazenam dados em nós mesmos. O banco de dados armazena dados em si mesmo, e isso pode ser o resultado de vários meses de operação do aplicativo. Além disso, esse banco de dados pode ser acessado não apenas pelo nosso aplicativo de back-end, mas também por muitos outros - pois também é um banco de dados, e não o iremos remontar constantemente. Novamente, esta é uma oportunidade de trabalhar com uma API externa, que, é claro, é conectar-se ao nosso banco de dados.

Montagem frontal


Para executar cada aplicativo (seja na frente ou atrás), você precisa de uma certa sequência de ações. Para executar o aplicativo no React, precisamos fazer o seguinte (desde que já tenhamos Linux):

  1. Instale o NodeJS.
  2. Copie o aplicativo para uma pasta específica.
  3. Instale os pacotes necessários (comando npm install).
  4. Inicie o aplicativo com o comando npm start.

É essa sequência de ações que precisamos executar na janela de encaixe. Para fazer isso, na raiz do projeto (no mesmo local que package.json está localizado), devemos colocar o Dockerfile com o seguinte conteúdo:

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

Vamos ver o que cada linha significa.

 FROM node:alpine 

Com essa linha, deixamos claro para a janela de encaixe que quando você inicia o contêiner, a primeira coisa que você precisa fazer é baixar o Docker do repositório e instalar o NodeJS e a mais leve (todas as versões mais leves de estruturas e bibliotecas populares na janela de encaixe são geralmente chamadas de alpinas).

 WORKDIR /usr/app/front 

No contêiner do Linux, as mesmas pastas padrão serão criadas como em outras pastas do Linux - / opt, / home, / etc, / usr e assim por diante. Definimos o diretório de trabalho com o qual iremos trabalhar - / usr / app / front.

 EXPOSE 3000 

Abrimos a porta 3000. Uma comunicação adicional com o aplicativo em execução no contêiner ocorrerá através dessa porta.

 COPY ./ ./ 

Copie o conteúdo do projeto de origem para a pasta de trabalho do contêiner.

 RUN npm install 

Instale todos os pacotes necessários para executar o aplicativo.

 CMD ["npm", "start"] 

Iniciamos o aplicativo com o comando npm start.

Este cenário será executado em nosso aplicativo quando o contêiner iniciar.

Vamos direto na frente. Para fazer isso, no terminal, estando na pasta raiz do projeto (onde o Dockerfile está localizado), execute o comando:

 docker build -t rebounder-chain-frontend . 

Valores de comando:

docker é uma chamada para o aplicativo docker, bem, você sabe disso.
construir - crie uma imagem a partir dos materiais de destino.
-t <name> - no futuro, o aplicativo estará disponível pela tag especificada aqui. Você pode omitir isso, o Docker gerará sua própria tag, mas será impossível diferenciá-lo dos outros.
. - indica que você precisa coletar o projeto da pasta atual.



Como resultado, a montagem deve terminar com o texto:

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

Se percebermos que a última etapa foi concluída e tudo foi bem-sucedido, temos uma imagem. Podemos verificar isso executando-o:

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

Acho que o significado desse comando é geralmente entendido, com exceção da entrada -p 8080: 3000.
docker run rebounder-chain-frontend - significa que estamos lançando uma imagem do docker, que chamamos de rebounder-chain-frontend. Mas esse contêiner não terá uma saída para o exterior, ele precisa definir uma porta. É a equipe abaixo que define isso. Lembramos que nosso aplicativo React é executado na porta 3000. O comando -p 8080: 3000 diz ao docker para pegar a porta 3000 e encaminhá-la para a porta 8080 (que será aberta). Portanto, um aplicativo executado na porta 3000 será aberto na porta 8080 e estará disponível na máquina local nessa porta.

 ,       : 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. 

Não deixe o registro incomodá-lo

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

Reagir pensa assim. Ele está realmente disponível no contêiner na porta 3000, mas encaminhamos essa porta para a porta 8080 e, a partir do contêiner, o aplicativo é executado na porta 8080. Você pode executar o aplicativo localmente e verificar isso.

Portanto, temos um contêiner pronto com um aplicativo front-end, agora vamos coletar o back-end.

Crie back-end.


O script para iniciar um aplicativo em Java é significativamente diferente do assembly anterior. Consiste nos seguintes itens:

  1. Instale a JVM.
  2. Nós coletamos o arquivo jar.
  3. Nós o lançamos.

No Dockerfile, esse processo se parece com o seguinte:

 # 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"] 

O processo de montagem de uma imagem com a inclusão de um dzharnik em alguns pontos se assemelha ao de nossa frente.

O processo de montagem e lançamento da segunda imagem é essencialmente o mesmo que montagem e lançamento da primeira imagem.

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

Agora, se você tiver os dois contêineres em execução e o back-end estiver conectado ao banco de dados, tudo funcionará. Lembro que você deve registrar a conexão com o banco de dados a partir do back-end e deve trabalhar através de uma rede externa.

3. Colete imagens e execute contêineres em um servidor remoto


Para que tudo funcione em um servidor remoto, precisamos do Docker já instalado nele, após o qual, basta executar as imagens. Seguiremos o caminho certo e enviaremos nossas imagens para nossa conta na nuvem do Docker, após o que elas estarão disponíveis em qualquer lugar do mundo. Obviamente, existem muitas alternativas para essa abordagem, bem como tudo o que é descrito no post, mas vamos avançar um pouco mais e fazer nosso trabalho bem. Ruim, como Andrei Mironov disse, sempre temos tempo para fazê-lo.

Criando uma conta no hub do Docker


A primeira coisa que você precisa fazer é obter uma conta no hub do Docker. Para fazer isso, vá para o hub e registre-se. Não é difícil.

Em seguida, precisamos ir ao terminal e fazer login no Docker.

 docker login 

Você será solicitado a inserir um nome de usuário e senha. Se tudo estiver ok, uma notificação aparecerá no terminal que o Login foi bem-sucedido.

Confirmando imagens no Docker Hub


Em seguida, precisamos marcar nossas imagens e enviá-las para o hub. Isso é feito pela equipe de acordo com o seguinte esquema:

 docker tag   /_: 

Portanto, precisamos especificar o nome da nossa imagem, login / repositório e a marca sob a qual nossa imagem será confirmada no hub.

No meu caso, ficou assim:



Podemos verificar a presença desta imagem no repositório local usando o comando:

 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 

Nossa imagem está pronta para ser confirmada. Confirmar:

 docker push xpendence/rebounder-chain-backend:0.0.2 

Um registro de confirmação bem-sucedido deve aparecer.
Faça o mesmo com o frontend:

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

Agora, se formos para hub.docker.com, veremos duas imagens bloqueadas. Que estão disponíveis em qualquer lugar.





Parabéns Nós apenas temos que passar para a parte final do nosso trabalho - para lançar imagens em um servidor remoto.

Executar imagens em um servidor remoto


Agora podemos executar nossa imagem em qualquer máquina com o Docker, preenchendo apenas uma linha no terminal (no nosso caso, precisamos executar sequencialmente duas linhas em terminais diferentes - uma para cada imagem).

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

Este lançamento tem, no entanto, um sinal de menos. Quando o terminal estiver fechado, o processo será encerrado e o aplicativo deixará de funcionar. Para evitar isso, podemos executar o aplicativo no modo "desanexado":

 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 

Agora, o aplicativo não emitirá um log para o terminal (isso pode ser configurado novamente), mas mesmo quando o terminal estiver fechado, ele não parará de funcionar.

4. Resolvendo problemas de rede


Se você fez tudo certo, pode esperar a maior decepção no caminho para seguir este post - pode acontecer que nada funcione. Por exemplo, tudo funcionou perfeitamente para você e funcionou na máquina local (como, por exemplo, no meu Mac), mas quando implantados em um servidor remoto, os contêineres pararam de se ver (como, por exemplo, no meu servidor remoto no Linux). Qual é o problema? Mas o problema é esse, e no começo eu sugeri isso. Como mencionado anteriormente, quando o contêiner é iniciado, o Docker cria uma máquina virtual separada, rola o Linux para lá e instala o aplicativo nesse Linux. Isso significa que o host local condicional para o contêiner em execução é limitado ao próprio contêiner e o aplicativo não está ciente da existência de outras redes. Mas precisamos:

a) os contêineres se viam.
b) o back-end viu o banco de dados.

Existem duas soluções para o problema.

1. Criando uma rede interna.
2. Trazendo contêineres para o nível do host.

1. No nível do Docker, você pode criar redes, além disso, três delas por padrão - bridge , none e host .

Bridge é uma rede interna do Docker isolada da rede host. Você pode acessar contêineres apenas pelas portas que você abre quando o contêiner inicia com o comando -p . Você pode criar qualquer número de redes, como ponte .



Nenhuma é uma rede separada para um contêiner específico.

Host é a rede host. Ao escolher essa rede, seu contêiner é totalmente acessível por meio do host - o comando -p simplesmente não funciona aqui e, se você implantou o contêiner nessa rede, não precisa especificar uma porta externa - o contêiner pode ser acessado por sua porta interna. Por exemplo, se o Dockerfile EXPOSE estiver definido como 8090, é por essa porta que o aplicativo estará disponível.



Como precisamos ter acesso ao banco de dados do servidor, usaremos o último método e colocaremos os contêineres na rede do servidor remoto.

Isso é feito com muita simplicidade, removemos a menção de portas do comando de inicialização do contêiner e especificamos a rede host:

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

Conexão à base que eu indiquei

 localhost:3306 

A conexão da frente à parte traseira tinha que ser especificada inteiramente, externa:

 http://<__:__> 

Se você encaminhar a porta interna para a porta externa, que geralmente é o caso de servidores remotos, será necessário especificar a porta interna para o banco de dados e a porta externa para o contêiner.

Se você quiser experimentar conexões, poderá fazer o download e criar um projeto que escrevi especialmente para testar a conexão entre contêineres. Basta digitar o endereço desejado, pressionar Enviar e, no modo de depuração, ver o que voou de volta.

O projeto está aqui .

Conclusão


Existem várias maneiras de criar e executar uma imagem do Docker. Para os interessados, aconselho você a aprender docker-compor. Aqui examinamos apenas uma das maneiras de trabalhar com o docker. Obviamente, essa abordagem a princípio não parece tão simples. Mas aqui está um exemplo - durante a escrita de uma postagem, eu tinha conexões de saída em um servidor remoto. E durante o processo de depuração, tive que alterar as configurações de conexão com o banco de dados várias vezes. Todo o conjunto e implantação se encaixam no meu conjunto de 4 linhas, depois de entrar, onde vi o resultado em um servidor remoto. No modo de programação extremo, o Docker é indispensável.

Como prometido, eu posto as fontes do aplicativo:

back-end
frontend

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


All Articles