«Нам нужен DevOps!»
(самая популярная фраза в конце любого хакатона)

Сначала немного лирики.

Когда разработчик является отличным девопсом, умеющим развернуть своё детище на любой машине под любой OC, это плюс. Однако, если он вообще ничего не смыслит дальше своей IDE, это не минус — в конце концов, деньги ему платят за код, а не за умение его разворачивать. Узкий глубокий специалист на рынке ценится выше, чем средней квалификации «мастер на все руки». Для таких, как мы, «пользователей IDE», хорошие люди придумали Docker.

Принцип Docker следующий: «работает у меня — работает везде». Единственная программа, необходимая для деплоя копии Вашего приложения где угодно — это Docker. Если Вы запустили своё приложение в докере у себя на машине, оно гарантированно с тем же успехом запустится в любом другом докере. И ничего, кроме докера, устанавливать не нужно. У меня, к примеру, на виртуальном сервере даже Java не стоит.

Как работает Docker?


Docker создаёт образ виртуальной машины с установленными в ней приложениями. Дальше этот образ разворачивается как абсолютно автономная виртуальная машина. Запущенная копия образа называется «контейнер». Вы можете запустить на сервере любое количество образов, и каждый из них будет отдельной виртуальной машиной со своим окружением.

Что такое виртуальная машина? Это инкапсулированное место на сервере с операционкой, в которой установлены приложения. В любой операционке обычно крутится большое количество приложений, в нашей же находится одно.

Схему развёртывания контейнеров можно представить так:



Для каждого приложения мы создаём свой образ, а потом разворачиваем каждый контейнер отдельно. Также, можно положить все приложения в один образ и развернуть как один контейнер. Причём, чтобы не разворачивать каждый контейнер отдельно, мы можем использовать отдельную утилиту docker-compose, которая конфигурирует контейнеры и взаимосвязь между ними через отдельный файл. Тогда структура всего приложения может выглядеть так:



Я намеренно не стал вносить базу данных в общую сборку Docker, по нескольким причинам. Во-первых, база данных полностью независима от приложений, которые с ней работают. Это может быть далеко не одно приложение, это могут быть ручные запросы из консоли. Лично я не вижу смысла ставить базу данных в зависимость от сборки Docker, в которой она находится. Поэтому я её и вынес. Впрочем, очень часто практикуется подход, при котором база данных помещается в отдельный образ и запускается отдельным контейнером. Во-вторых, хочется показать, как Docker-контейнер взаимодействует с системами вне контейнера.

Впрочем, довольно лирики, давайте писать код. Мы напишем простейшее приложение на спринге и реакте, которое будет записывать наши обращения к фронту в базу данных, и поднимем всё это через Docker. Структура нашего приложения будет выглядеть так:



Реализовать такую структуру можно разными путями. Мы реализуем один из них. Мы создадим два образа, запустим из них два контейнера, причём, бэкенд будет подключаться к базе данных, которая установлена на конкретном сервере где-то в интернете (да, такие запросы к базе будут ходить не быстро, но нами движет не жажда оптимизации, а научный интерес).

Пост будет разбит на части:

0. Устанавливаем Docker.
1. Пишем приложения.
2. Собираем образы и запускаем контейнеры.
3. Собираем образы и запускаем контейнеры на удалённом сервере.
4. Решаем проблемы с сетью.

0. Устанавливаем Docker


Для того, чтобы установить Docker, нужно зайти на сайт и следовать тому, что там написано. При установка Docker на удалённом сервере будьте готовы к тому, что с серверами на OpenVZ Docker может не работать. Равно как могут быть проблемы, если у Вас не включён iptables. Желательно заводить сервер на KVM с iptables. Но это мои рекомендации. Если у Вас всё заработает и так, я буду рад, что Вы не потратили кучу времени на выяснение, почему не работает, как это пришлось сделать мне.

1. Пишем приложения


Напишем простое приложение с самым примитивным бэкендом на Spring Boot, очень простым фронтендом на ReactJS и базой данных MySQL. Приложение будет иметь Single-Page с одной-единственной кнопкой, которая будет записывать время нажатия по ней в базу данных.

Я рассчитываю на то, что Вы уже умеете писать приложения на буте, но если нет, Вы можете клонировать готовый проект. Все ссылки в конце статьи.

Backend на 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'
}

Сущность 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, который будет работать по упрощённой логике и сразу писать в базу данных. Сервис мы опускаем.

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

Всё, как мы видим, очень просто. По GET-запросу мы делаем запись в базу и возвращаем результат.

Файл настроек приложения обсудим отдельно. Их два.

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

Как это работает, Вы, наверняка, знаете, сначала Spring сканирует файл application.properties или application.yml — какой найдёт. В нём мы указываем одну-единственную настройку — какой профиль будем использовать. Обычно во время разработки у меня накапливается несколько профилей, и очень удобно их переключать при помощи дефолтного профиля. Далее Spring находит application.yml с нужным суффиксом и пользует его.

Мы указали датасорс, настройки JPA и, что важно, внешний порт нашего бэкенда.

Фронтенд на ReactJS


Фронтенд тоже можно посмотреть в проекте на git, а можно даже не смотреть, а клонировать и запустить.

Отдельную работу фронтенда можно проверить, скачав проект, перейдя в терминале в корневую папку проекта (туда, где лежит файл package.json) и выполнив последовательно две команды:

npm install    // устанавливает в проект все необходимые зависимости, аналог maven
npm start      // запускает проект

Конечно, для этого Вам нужен установленный Node Package Manager (npm), и это тот самый трудный путь, которого мы избегаем при помощи Docker. Если Вы всё-таки запустили проект, Вы увидите следующее окошко:



Ну да ладно, настало время посмотреть код. Укажу лишь часть, которая обращается к бэкенду.

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

Фронтенд работает предсказуемо. Переходим по ссылке, дожидаемся ответа и выводим его на экран.



Стоит акцентировать внимание на следующих пунктах:

  1. Фронт открыт внешнему миру через порт 3000. Это порт по умолчанию для React.
  2. Бэк открыт по порту 8099. Мы его задали в настройках приложения.
  3. Бэк стучится к БД через внешний интернет.

Приложение готово.

2. Собираем образы и запускаем контейнеры


Структура нашей сборки будет следующая. Мы создадим два образа — фронтенд и бэкенд, которые будут общаться друг с другом через внешние порты. Для базы мы не будем создавать образ, мы установим её отдельно. Почему так? Почему для базы мы не создаём образ? У нас есть два приложения, которые постоянно изменяются и не хранят в себе данные. База данных хранит в себе данные, и это может быть результат нескольких месяцев работы приложения. Более того, к данной базе данных может обращаться не только наше бэкенд-приложение, но и многие другие — на то она и база данных, и мы не будем её постоянно пересобирать. Опять же, это возможность поработать с внешним API, чем, безусловно, является подключение к нашей БД.

Сборка front-end


Для запуска каждого приложения (будь то фронт или бэк) потребуется определённая последовательность действий. Для запуска приложения на React нам потребуется сделать следующее (при условии, что у нас уже есть Linux):

  1. Установить NodeJS.
  2. Скопировать приложение в определённую папку.
  3. Проинсталлировать необходимые пакеты (команда npm install).
  4. Запустить приложение командой npm start.

Именно эту последовательность действий нам и предстоит выполнить в докере. Для этого в корне проекта (там же, где находится package.json) мы должны разместить файл Dockerfile со следующим содержанием:

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

Разберём, что означает каждая строчка.

FROM node:alpine

Этой строчкой мы даём понять докеру, что при запуске контейнера первым делом нужно будет скачать из репозитория Docker и установить NodeJS, причём, самую лёгкую (все самые лёгкие версии популярных фреймворков и библиотек в докере принято называть alpine).

WORKDIR /usr/app/front

В линуксе контейнера будут созданы те же стандартные папки, что и в других линуксах — /opt, /home, /etc, /usr и проч. Мы задаём рабочую директорию, с которой будем работать — /usr/app/front.

EXPOSE 3000

Открываем порт 3000. Дальнейшая связь с приложением, запущенным в контейнере, будет происходить через этот порт.

COPY ./ ./

Копируем содержимое исходного проекта в рабочую папку контейнера.

RUN npm install

Устанавливаем все пакеты, необходимые для запуска приложения.

CMD ["npm", "start"]

Запускаем приложение командой npm start.

Именно этот сценарий будет выполнен в нашем приложении при запуске контейнера.

Давайте сразу соберём фронт. Для этого нужно в терминале, находясь в корневой папке проекта (там, где находится Dockerfile), выполнить команду:

docker build -t rebounder-chain-frontend .

Значения команд:

docker — вызов приложения docker, ну, это вы знаете.
build — сборка образа из целевых материалов.
-t <имя> — в дальнейшем, приложение будет доступно по тегу, указанному здесь. Можно не указывать, тогда Docker сгенерирует собственный тег, но отличить его от других будет невозможно.
. — указывает, что собирать проект нужно именно из текущей папки.



В результате, сборка должна закончиться текстом:

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

Если мы видим, что последний шаг выполнен и всё Successfull, значит, образ у нас есть. Мы можем проверить это, запустив его:

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

Смысл этой команды, я думаю, в целом понятен, за исключением записи -p 8080:3000.
docker run rebounder-chain-frontend — означает, что мы запускаем такой-то докер-образ, который мы обозвали rebounder-chain-frontend. Но такой контейнер не будет иметь выхода наружу, ему нужно задать порт. Именно команда ниже его задаёт. Мы помним, что наше React-приложение запускается на порте 3000. Команда -p 8080:3000 указывает докеру, что нужно взять порт 3000 и пробросить его на порт 8080 (который будет открыт). Таким образом, приложение, которое работает по порту 3000, будет открыто по порту 8080, и на локальной машине будет доступно именно по этому порту.

Итак, что мы видим при запуске команды выше:

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.

Пусть вас не смущает запись

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

Так думает React. Он действительно доступен в пределах контейнера по порту 3000, но мы пробросили этот порт на порт 8080, и из контейнера приложение работает по порту 8080. Можете запустить приложение локально и проверить это.

Итак, у нас есть готовый контейнер с фронтенд-приложением, теперь давайте соберём бэкенд.

Сборка back-end.


Сценарий запуска приложения на Java существенно отличается от предыдущей сборки. Он состоит из следующих пунктов:

  1. Устанавливаем JVM.
  2. Собираем jar-архив.
  3. Запускаем его.

В Dockerfile этот процесс выглядит так:

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

Процесс сборки образа с включением джарника по некоторым пунктам напоминает таковой для нашего фронта.

Процесс сборки и запуска второго образа принципиально ничем не отличается от сборки и запуска первого.

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

Теперь, если у Вас запущены оба контейнера, а бэкенд подключён к БД, всё заработает. Напоминаю, что подключение к БД из бэкенда Вы должны прописать сами, и оно должно работать через внешнюю сеть.

3. Собираем образы и запускаем контейнеры на удалённом сервере


Для того, чтобы всё заработало на удалённом сервере, нам необходимо, чтобы на нём был уже установлен Docker, после чего, достаточно запустить образы. Мы пойдём правильным путём и закоммитим наши образы в свой аккаунт в облаке Docker, после чего, они станут доступны из любой точки мира. Конечно, альтернатив данному подходу, как и всему, что описано в посте, предостаточно, но давайте ещё немного поднажмём и сделаем свою работу хорошо. Плохо, как говорил Андрей Миронов, мы всегда успеем сделать.

Создание аккаунта на хабе Docker


Первое, что Вам предстоит сделать — это обзавестись аккаунтом на хабе Docker. Для этого надо перейти на хаб и зарегаться. Это несложно.

Далее, нам нужно зайти в терминал и авторизоваться в Docker.

docker login

Вас попросят ввести логин и пароль. Если всё ок, в терминале появится уведомление, что Login Succeeded.

Коммит образов на Docker Hub


Далее, мы должны пометить наши образы тэгами и закоммитить их в хаб. Делается это командой по следующей схеме:

docker tag имя образа логин/имя_образа:версия

Таким образом, нам нужно указать имя нашего образа, логин/репозиторий и тэг, под которым наш образ будет закоммичен в хаб.

В моём случае, это выглядело так:



Мы можем проверить наличие данного образа в локальном репозитории при помощи команды:

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

Наш образ готов к коммиту. Коммитим:

docker push xpendence/rebounder-chain-backend:0.0.2

Должна появиться запись об успешном коммите.
Делаем то же самое с фронтендом:

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

Теперь, если мы зайдём на hub.docker.com, мы увидим два закоммиченных образа. Которые доступны откуда угодно.





Поздравляю. Нам осталось перейди к заключительной части нашей работы — запустить образы на удалённом сервере.

Запускаем образы на удалённом сервере


Теперь мы можем запустить наш образ на любой машине с Docker, выполнив всего одну строчку в терминале (в нашем случае, нам надо последовательно выполнить две строчки в разных терминалах — по одной на каждый образ).

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

У такого запуска есть, правда, один минус. При закрытии терминала процесс завершится и приложение прекратит работу. Чтобы этого избежать, мы можем запустить приложение в «откреплённом» режиме:

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

Теперь приложение не будет выдавать лог в терминал (это можно, опять же, настроить отдельно), но и при закрытии терминала оно не прекратит свою работу.

4. Решаем проблемы с сетью


Если Вы всё сделали правильно, Вас, возможно, ожидает самое большое разочарование на всём пути следования этому посту — вполне может так получиться, что ничего не работает. Например, у Вас всё великолепно собралось и заработало на локальной машине (как, например, у меня на маке), но при развёртывании на удалённом сервере контейнеры перестали друг друга видеть (как, например, у меня на удалённом сервере на Linux). В чём проблема? А проблема вот в чём, и я в начале о ней намекал. Как уже было сказано раньше, при запуске контейнера Docker создаёт отдельную виртуальную машину, накатывает туда Linux, и потом в этот Linux устанавливает приложение. Это значит, что условный localhost для запущенного контейнера ограничивается самим контейнером, и о существовании других сетей приложение не подозревает. Но нам нужно, чтобы:

а) контейнеры видели друг друга.
б) бэкенд видел базу данных.

Решения проблемы два.

1. Создание внутренней сети.
2. Вывод контейнеров на уровень хоста.

1. На уровне Docker можно создавать сети, причём, три из них в нём есть по умолчанию — bridge, none и host.

Bridge — это внутренняя сеть Docker, изолированная от сети хоста. Вы можете иметь доступ к контейнерам только через те порты, которые открываете при запуске контейнера командой -p. Можно создавать любое количество сетей типа bridge.



None — это отдельная сеть для конкретного контейнера.

Host — это сеть хоста. При выборе этой сети, Ваш контейнер полностью доступен через хост — здесь попросту не работает команда -p, и если Вы развернули контейнер в этой сети, то Вам незачем задавать внешний порт — контейнер доступен по своему внутреннему порту. Например, если в Dockerfile EXPOSE задан как 8090, именно через этот порт будет доступно приложение.



Поскольку нам нужно иметь доступ к базе данных сервера, мы воспользуемся последним способом и выложим контейнеры в сеть удалённого сервера.

Делается это очень просто, мы убираем из команды запуска контейнера упоминание о портах и и указываем сеть host:

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

Подключение к базе я указал

localhost:3306

Подключение фронта к бэку пришлось указать целиком, внешнее:

http://<хост_удалённого_сервера:порт_удалённого_сервера>

Если вы пробрасываете внутренний порт на внешний, что часто бывает для удалённых серверов, то для базы нужно указывать внутренний порт, а для контейнера — внешний порт.

Если Вы хотите поэкспериментировать с подключениями, вы можете скачать и собрать проект, который я специально написал для тестирования соединения между контейнерами. Просто вводите необходимый адрес, жмёте Send и в режиме отладки смотрите, что прилетело обратно.

Проект лежит тут.

Заключение


Есть масса способов собрать и запустить образ Docker. Интересующимся советую изучить docker-compose. Здесь мы разобрали лишь один из способов работы с докером. Конечно, такой подход поначалу кажется не таким уж и простым. Но вот пример — в ходе написания поста у меня возникли с исходящими подключениями на удалённом сервере. И в процессе дебага мне пришлось несколько раз менять настройки подключения к БД. Вся сборка и деплой умещались у меня в набор 4 строчек, после ввода которых я видел результат на удалённом сервере. В режиме экстремального программирования Docker окажется незаменим.

Как и обещал, выкладываю исходники приложений:

backend
frontend

Комментарии (37)


  1. yurror
    16.05.2019 07:34

    1) Всегда думал что контейнеры это не виртуальные машины, а, ээмм… контейнеры?
    2) Всегда думал что контейнер создается набором средств (namespace, cgroups, что там еще?) ядра linux (да, я знаю про WSLv1), а внутри контейнера просто юзерленд содержится.
    Что бы не поседеть надо понимать суть вещей, я считаю.


    1. fessmage
      16.05.2019 07:58

      Точно, кажется уже любой гайд по докеру начинается с "докер — не ВМ", и всё равно такое мнение берется откуда-то.
      Плюс вредные советы вроде открытия контейнеров в сеть хоста, вместо создания отдельных docker network — одной для связи между фронтом и беком, и одной между беком и бд. В docker-compose это две строчки.


      1. Nikobraz
        16.05.2019 10:36

        <Осторожно! Вредный совет!> в контейнер еще по ssh ходить можно)


      1. gecube
        16.05.2019 12:55

        Насчёт открывать докеры в сеть хоста — это не вредный совет, а просто инструмент для решения задачи. Использовать его или нет — нужно решить самому в каждом конкретном случае


    1. Adverte
      16.05.2019 11:13

      там много ещё такого по тексту статьи.
      Собственно автор 2262288@gmail.com Ивент-агентство Арлекино и этим всё сказано


      1. xpendence Автор
        16.05.2019 11:44

        Очень приятно, что Вы интересуетесь моими профессиональными успехами. Но завидовать надо скромнее, мне кажется. Также, приглашаю Вас посетить мой профессиональный сайт developer.xpendence.ru и на мою страницу в ВК, где Вы сможете поделиться своей историей успеха.


        1. Nikobraz
          17.05.2019 17:16

          CI/CD — Docker???


  1. dipiash
    16.05.2019 08:37

    Это не совсем по теме стать, но относится к приведенному примеру.
    Пожалуйста не делайте так:

     return await res.json();
    

    Почему можно посмотреть тут:
    1. Why Using `return await` Is a Bad Idea?
    2. Disallows unnecessary return await (no-return-await)


    1. xpendence Автор
      16.05.2019 08:49

      Интересно, спасибо. То есть, мы обрабатываем Promice в том же методе и возвращаем только готовый ответ, дождавшись его?


      1. dipiash
        16.05.2019 10:07
        +1

        Ваш вариант:

        const getResource = async () => {
                const res = await fetch(JSON_URL);
        
                return await res.json();
            };


        И этот вариант:
        const getResource1 = async () => {
                const res = await fetch('https://api.zaycev.fm/api/v1/channels');
        
                return res.json();
            };
        


        Вернут одинаковые результаты.
        Однако результат асинхронной функции уже завернут в Promise.resolve (не явно). Т.о. в первой функции дополнительный await накладывает небольшой оверхед — по сути делается таже самая работа 2а раза.


        1. xpendence Автор
          16.05.2019 10:50

          Да, действительно. Спасибо, поправил пример.


        1. Jofr
          17.05.2019 10:25

          Пожалуйста, не пишите так: '2a'. Вы накладываете небольшой оверхед на числительное, — то же самое окончание, по сути, пишется два раза.

          Окончание необходимо только для порядковых (второй как 2-й) числительных.


  1. Daar
    16.05.2019 10:09

    Ну хз, даже судя по нашим и соседям: Узкий глубокий специалист + devops = +20-30% к ЗП и большая любовь суппорта.


  1. xkondorx
    16.05.2019 10:29
    +1

    Также, можно положить все приложения в один образ и развернуть как один контейнер.

    Это плохая практика, так делать не надо… Зависнет у Вас клиент на NodeJs, а перезагружать придется и бек тоже, лишние ресурсы на запуск бэка (который не требовалось перезагружать вообще), создание для себя лишних трудностей (и героическое их преодоление) когда возникнет потребность в балансировке нагрузки, из-за жесткой связи — один экземпляр клиента к одному экземпляру сервера.
    Я не специалист в NodeJs(вообще), объясните кто ни будь, зачем он в этой схеме, какую задачу он должен решать? Из этой схемы мне на ум приходит:
    — Серверный рендеринг страниц
    — Раз взаимодействие с бэком на java идет за ним, то это некий Gateway, значит он несет в себе еще и функционал валидации запросов и наверно аутентификацию этих запросов.
    Я ни в коем случае не придираюсь, и осознаю, что это просто пример, но это не пример из практики и никто так делать скорее всего не будет (правда ведь не будет?), а значит такую статью сложно осмыслить для практического применения теми людьми для которых эта статья написана.


    1. xpendence Автор
      16.05.2019 10:33

      В примере я разбираю React-приложение, стало быть, для того, чтобы развернуть React-приложение, да и любое приложение, которое рендерит фронт, нам нужно будет запустить его через npm. Очень сомневаюсь, что Вы не сталкивались с примерами из практики, когда фронт собирается отдельно на том же реакте, бэк и фронт лежат в разных микросервисах и они общаются через REST. Конечно, за исключением случаев, когда бэк на Java рендерит JSP, но, я надеюсь, Вы ведь не об этом?

      На протяжении своей профессиональной деятельности я не раз сталкивался с таким подходом, вот и сейчас мы пишем приложение на докере с полутора десятками микросервисов, которые собираются, правда, через OpenShift (но это уже другая история), но внутри тот же докер. Если говорить о хакатонах, то подход, когда каждый пишет свой микросервис, а потом всё это собирается отдельно и общается через REST — вообще, на мой взгляд, самый рабочий.


      1. xkondorx
        16.05.2019 11:06

        В моей практике клиент собирается/упаковывается во время CI/CD с помощью webpack, формируется набор статических файлов которые помещаются в контейнер с вебсервером и ссылкой на эти файлы. С тем, что Вы сейчас пишите я согласен, но почему на схеме вы указываете взаимодействие NodeJs с Java сервером внутри контейнера или внутри виртуальной сети докера? Я понимаю это так, что кроме сборки клиента пользователю NodeJs принимает REST запросы от клиента и делегирует их выполнение через себя на java сервер.


        1. xkondorx
          16.05.2019 11:11

          Написав это, я подумал, что возможно у Вас очень большой клиент и Вы не хотите отдавать его весь сразу (много файлов, трафика и все такое) и нашли такой выход из ситуации как сборка налету.


        1. mayorovp
          16.05.2019 12:32

          У современных веб-фреймворков существует такая фича, как SSR — рендер на стороне сервера. Он может использоваться для ускорения загрузки страницы, для поддержки браузеров без javascript и для снятия проблем индексации поисковиками.


          Поскольку эта штука исполняет клиентский javascript на стороне сервера, ей конечно же нужен рантайм. Например, nodejs.


          Кто к кому должен при этом обращаться с запросами (нода к бакенду или бакенд к ноде) — не принципиально, возможны оба варианта.


          1. xkondorx
            16.05.2019 12:42

            Другими словами это пример не с SPA.


            1. mayorovp
              16.05.2019 12:44

              Почему же? Как раз для SPA серверный пререндер является стандартной оптимизацией.


              1. xkondorx
                16.05.2019 13:30

                Если бы на схеме API был опубликован для вызова из внешнего источника, то я бы увидел здесь SPA, на в данном случае, пользователь получает сформированные страницы с NodeJs, это не опровергает того факта, что со страницы могут идти REST запросы на NodeJs которые будут делегироваться java серверу. Однако подобный подход мне кажется спорным.


                1. krendel122
                  16.05.2019 15:25

                  Вы все верно поняли, я тоже не вижу смысла отдавать статику нодой, а не бекендом, если не нужен ssr и всякие варианты с проксированием.


  1. t1gger
    16.05.2019 10:51
    +1

    прочитав в заголовке про фуллстек, ожидал увидеть про docker-compose, но увы.
    рекомендую в данной задаче попробовать именно его — тогда и запускать будет легче, и сеть можно будет легко настроить


    1. xpendence Автор
      16.05.2019 10:51

      Да, спасибо. Хотел включить ещё и docker-compose, но статья и так раздулась до каких-то нереальных размеров.


      1. vassabi
        16.05.2019 12:29

        намек на продолжение (или даже серию статей)?


        1. xpendence Автор
          16.05.2019 12:37

          Думаю, есть смысл дополнить статью ещё одной, где можно будет раскрыть эти темы.


  1. Vest
    16.05.2019 12:00

    Меня всегда в таких случаях интересует, как человек пишет и отлаживает код. Для меня это всегда итеративный процесс, а тут получается, без привязки к IDE создали бекенд с профилем, где мы подключаемся к некой базе данных. Причём, БД не подняли ещё, а приложение уже запустили и отладили.

    Я не специалист в контейнерах, но когда работал с Cloud Foundry ощутил все эти неудобства, когда приходилось прыгать между IDE, и поднимать то одно приложение локально, то другое, потом делать где-то изменения, потом подключаться к отладчикам, потом решал проблемы с CORS, если не подумал о них заранее… А в вашей статье (спасибо ещё раз), как-то всё просто. Вставили этот код, тот, запустили и всё работает.

    Извините, но мне бы хотелось посмотреть на статью как разработчик организовывает свой рабочий процесс этого небольшого приложения. Вот это было бы интересно. Может быть у вас есть портянка скриптов для локальной разработки, о которой вы просто не сообщили :)


    1. vassabi
      16.05.2019 12:32

      ну вот у меня почти похожий (джава+ангуляр) случай. Вся пачка проектов (всякие core, api1, api2 бекенда и фронтенд) открыта в одной единственной ИДЕ — IntelliJ IDEA, отладка там же локально
      (ок, ng serve --poll 2000 — просто запущен в другом терминале), а в докер кладутся уже бинари (и поднимаются для тестов\продакшена у амазона)


    1. dididididi
      16.05.2019 13:14

      База там поднята, я правда, не понял как))) Какая-то новая спрингбутовая магия.
      Все в Интелиджи поднимется. Npm start для фронта и зеленый треугольничек для бэка. А профиля, он поэтому все под хостовой сетью и запустил в докере, чтоб с профилями и портами не трахаццо, наверно.


    1. xpendence Автор
      16.05.2019 13:31

      Да, рабочий процесс был очень интересен, учитывая, что в реакте я пока новичок, но если бы я взялся описывать ещё и его, статья была бы просто нечитаемой из-за своего размера :) Но сфокусироваться хотелось на докере, поэтому я и опустил все эти моменты :)


      1. Vest
        16.05.2019 13:36

        Вы всё разрабатывали в одной IDE, как описали ребята выше? Например, IntelliJ IDEA, или у вас было постоянное переключение между редакторами, консолью и прочее.


        1. xpendence Автор
          16.05.2019 13:48

          Всё написал в IDEA, образы собирал и коммитил во встроенном в IDEA терминале, а деплоил на удалённый сервер через терминал.


    1. qant
      17.05.2019 07:30

      docker позволяет линковать локальные файлы внутрь контейнера. docker run -v /host/directory:/container/directory… таким образом вы сразу можете видеть изменения внутри контейнера


      1. gecube
        17.05.2019 08:50

        Это не очень хороший совет. Фишка в том, что если каталог /host/directory не создан, то докер его создаст сам с правами от рута. Ну, и далее начинается вакханалия. В теории — лучше пользоваться расширенным синтаксисом bind mount и подготавливать каталоги самому с нужными правами.
        Ну, или как вариант — использовать volume для данных.


  1. kalbas
    16.05.2019 12:03

    Девопс — это не специализация.
    Контейнер — это не виртуальная машина.

    Дальше даже читать не хочется.


    1. gecube
      16.05.2019 13:00

      Примерно аналогичные чувства. Но за что дальше зацепился взгляд — так это за EXPOSE в докерфайле. Его суть абсолютно не раскрыта, а именно — как правило — это тупо аннотация, вроде документации. По факту же у него есть два применения (docker run -P и ещё что-то аналогичное), в остальном — на прохождение пакетов и доступность портов между сервисами не влияет


    1. xpendence Автор
      16.05.2019 13:26
      +1

      Спасибо, что сообщили об этом.