Локальное окружение для разработки Spring Boot веб-сервисов с Docker Compose, Consul, Make

В статье представлен пример локального окружения, построенного с использованием технологий: Docker Compose, Consul, Make — для разработки Spring Boot веб-сервисов.


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


План:



1. Настройка сервисов в Docker Compose


Docker Compose — это инструмент для описания и запуска набора Docker-контейнеров. Настройка Docker Compose осуществляется в файле docker-compose.yml, который использует формат представления данных YAML.


Необходимо его создать — touch docker-compose.yml. В данном случае в файле достаточно двух секций:


  • версия конфигурации version,
  • настройки контейнеров с сервисами services.

В секции services описаны три сервиса: Consul, PostgreSQL, Browserless. Для каждого сервиса указывается образ контейнера image, имя хоста hostname, соответствие портов внутри контейнера портам хостовой машины ports.


PostgreSQL требует установки нескольких переменных среды: POSTGRES_DB — имя БД, POSTGRES_PASSWORD — пароль для подключения к базе данных пользователя по умолчанию postgres. Переменные среды часто используются для конфигурации сервисов внутри контейнера.


docker-compose.yml
version: '3.4'

services:
  consul:
    image: consul:1.1.0
    hostname: localhost
    ports:
      - 8500:8500
  postgres:
    image: postgres:11.0
    hostname: localhost
    ports:
      - 5432:5432
    environment:
      POSTGRES_PASSWORD: example_pass
      POSTGRES_DB: example_app
  browserless:
    image: browserless/chrome
    hostname: localhost
    ports:
      - 3000:3000

Чтобы запустить сервисы необходимо в директории с docker-compose.yml выполнить команду docker-compose up. При первом запуске будут загружены необходимые образы контейнеров, поэтому выполнение команды может занять некоторое время.


После запуска всех контейнеров необходимо в браузере загрузить страницу веб-клиента Consul по адресу localhost:8500. На данном этапе среди зарегистрированных сервисов будет отображаться только Consul:


consul services


Чтобы остановить контейнеры можно воспользоваться командой docker-compose down.


Дополнительную информацию по Docker Compose можно найти здесь.


2. Регистрация сервисов в Consul


Для обнаружения сервисов в приложении через Consul их нужно зарегистрировать. Регистрацию сервиса можно выполнить через HTTP API Consul, отправив POST-запрос на http://localhost:8500/v1/agent/service/register. Тело запроса должно содержать описание сервиса в виде JSON-объекта с полями:


  • Name, ID, Tags — имя, идентификатор, теги сервиса;
  • Address, Port — имя хоста и порт, на которых запущен сервис;
  • Check — данные для проверки статуса сервиса.

Значением поля Check является JSON-объект с полями:


  • Name, ID — имя и идентификатор проверки;
  • Interval — интервал опроса сервиса;
  • Timeout — таймаут запроса к сервису;
  • TCP или HTTP — конечная точка для проверки статуса сервиса, в зависимости от используемого протокола используются разные поля;
  • Status — необходимый статус, для успешного прохождения проверки.

Удобно все запросы к Consul выполнять через curl и сложить их в один скрипт.


register-services.sh
#!/bin/bash

curl -s -XPUT -d"{
  \"Name\": \"postgres\",
  \"ID\": \"postgres\",
  \"Tags\": [ \"postgres\" ],
  \"Address\": \"localhost\",
  \"Port\": 5432,
  \"Check\": {
    \"Name\": \"PostgreSQL TCP on port 5432\",
    \"ID\": \"postgres\",
    \"Interval\": \"10s\",
    \"TCP\": \"postgres:5432\",
    \"Timeout\": \"1s\",
    \"Status\": \"passing\"
  }
}" localhost:8500/v1/agent/service/register

curl -s -XPUT -d"{
  \"Name\": \"browserless\",
  \"ID\": \"browserless\",
  \"Tags\": [ \"browserless\" ],
  \"Address\": \"localhost\",
  \"Port\": 3000,
  \"Check\": {
    \"Name\": \"Browserless TCP on port 3000\",
    \"ID\": \"browserless\",
    \"Interval\": \"10s\",
    \"TCP\": \"browserless:3000\",
    \"Timeout\": \"1s\",
    \"Status\": \"passing\"
  }
}" localhost:8500/v1/agent/service/register

Необходимо обратить внимание на то, что изначально файл скрипта не является исполняемым, если это не задано при создании. Чтобы сделать его исполняемым нужно выполнить chmod +x register-services.sh.


После выполнения скрипта register-services.sh в списке зарегистрированных сервисов в Consul появятся PostgreSQL и Browserless:


more consul services


3. Добавление значений в хранилище Consul


Consul предоставляет key-value хранилище, которое можно использовать для хранения конфигураций приложения. В данном случае в нём будут храниться параметры для подключения к БД: имя БД, пользователь и пароль.


В реальном приложении для хранения секретных значений лучше использовать Vault — так же, как и Consul, разработан компанией HashiCorp и представляет собой хранилище секретных значений: паролей, ключей и т. д.


Для добавления значения в хранилище необходимо отправить PUT-запрос на http://localhost:8500/v1/kv/some/your/property, где some/your/property — это путь к значению. Тело запроса должно содержать само значение.


Необходимые приложению значения можно добавить командами:


# Добавление имени БД
curl --request PUT --data example_app localhost:8500/v1/kv/example.app/db/name

# Добавление имени пользователя
curl --request PUT --data postgres localhost:8500/v1/kv/example.app/db/username

# Добавление пароля
curl --request PUT --data example_pass localhost:8500/v1/kv/example.app/db/password

Опция --data используется для установки значения тела запроса.


Добавленные в хранилище значения можно найти на вкладке Key/Value веб-клиента Consul:


key-value consul storage


Команды удобно поместить в отдельный скрипт, а при наличии большего числа значений лучше написать загрузчик из YAML или JSON файла.


4. Создание Makefile


Внимание: Данный раздел скорее для пользователей Linux или MacOS, т. к. make изначально недоступен на Windows. Однако прочитать его стоит, потому что здесь описаны используемые далее команды для работы с проектом.


Make — это инструмент, позволяющий собрать команды для управления проектом в одно место и позволяющий абстрагироваться от инструментов языка разработки, например, от Maven или Gradle для Java или от yarn или npm для JavaScript.


Makefile — файл, содержащий все команды проекта, с его форматом можно ознакомиться здесь.


В случае демонстрационного проекта можно добавить команды для запуска контейнеров docker_up, инициализации Consul consul_up, компиляции compile и запуска run приложения и команду для запуска всего в одну строку up, для остановки окружения — команду down, которая останавливает и удаляет контейнеры.


Makefile
docker_up:
    @docker-compose up -d

consul_up:
    @./register-services.sh && \
     ./register-variables.sh

compile: 
    @cd example.app && mvn package

run:
    @cd example.app && java -jar target/example.app-1.0-SNAPSHOT.jar

up: docker_up consul_up compile run

down:
    @docker-compose down

Внимание: в Makefile для отступов используется tab.


5. Инициализация проекта


Дальнейшая работа требует Spring Boot Maven проект, который можно создать в генераторе.


Необходимые зависимости

org.springframework.boot:


  • spring-boot-starter — основной модуль Spring Boot;
  • spring-boot-starter-data-jpa — работа с хранилищами, например, PostgreSQL;
  • spring-boot-starter-web — веб-модуль;
  • spring-boot-starter-actuator — метрики, мониторинг приложения и т. д.

org.springframework.cloud:


  • spring-cloud-starter-consul-config — использование Consul как источник конфигураций;
  • spring-cloud-starter-consul-discovery — обнаружение сервисов через Consul;
  • spring-cloud-starter-openfeign — FeignClient.

org.postgresql:postgresql — клиентский модуль для работы с PostgreSQL.


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


Необходимо создать главный класс приложения с методом main. Класс должен быть помечен аннотациями SpringBootApplication и EnableFeignClients.


Application.java
@SpringBootApplication
@EnableFeignClients
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

Также необходимо указать некоторые свойства для Consul, FeignClient и Spring JPA в bootstrap.properties и application.yml:


bootstrap.properties

Настройка Consul и Spring JPA.


spring.application.name=example_app

spring.cloud.consul.host=localhost
spring.cloud.consul.port=8500
spring.cloud.consul.config.enabled=true
spring.cloud.consul.config.prefix=
spring.cloud.consul.config.defaultContext=example.app

spring.jpa.properties.hibernate.temp.use_jdbc_metadata_defaults=false
spring.jpa.hibernate.ddl-auto=update

application.yml

Установка таймаутов для запросов через FeignClient.


feign:
  client:
    config:
      default:
        connectTimeout: 20000
        readTimeout: 20000

6. Обнаружение PostgreSQL через Consul


Для работы с PostgreSQL необходимо написать конфигурацию, которая представляет собой инициализированный объект класса DataSource. Его инициализация требует некоторых значений, которые можно получить из Consul: имя БД databaseName, пользователь databaseUsername и пароль databasePassword — будут загружены из key-value хранилища Consul, а также хост и порт, на которых запущен PostgreSQL — будут получены из данных о зарегистрированных сервисах в Consul.


PersistenceConfiguration.java
@Configuration
public class PersistenceConfiguration {

    @Value("${example.app.db.name}")
    private String databaseName;

    @Value("${example.app.db.username}")
    private String databaseUsername;

    @Value("${example.app.db.password}")
    private String databasePassword;

    @Autowired
    private DiscoveryClient discoveryClient;

    @Bean
    @Primary
    public DataSource dataSource() {
        var postgresInstance = getPostgresInstance();

        return DataSourceBuilder
            .create()
            .username(databaseUsername)
            .password(databasePassword)
            .url(format("jdbc:postgresql://%s:%s/%s", postgresInstance.getHost(), postgresInstance.getPort(), databaseName))
            .driverClassName("org.postgresql.Driver")
            .build();
    }

    private ServiceInstance getPostgresInstance() {     
        return discoveryClient.getInstances("postgres")
        .stream()
        .findFirst()
        .orElseThrow(() -> new IllegalStateException("Unable to discover a Postgres instance"));
    }
}   

Метод getPostgresInstance(), используя DiscoveryClient, находит зарегистрированные в Consul сервисы с тегом postgres. Обнаруженные сервисы представляются объектами типа ServiceInstance, в котором содержатся данные о конкретном сервисе, включая его хост и порт.


Далее необходимо создать репозиторий для сущности Image, который предоставит методы для сохранения, обновления, удаления, выборки объектов данного типа:


ImageRepository.java
@Repository
public interface ImageRepository extends JpaRepository<Image, Long> {}

Класс Image содержит генерируемый PostgreSQL числовой идентификатор и две ссылки: на страницу и на обнаруженное на ней изображение:


Image.java
@Entity
@Table(name = "images")
public class Image {
    @Id
    @GeneratedValue
    private Long id;

    @NotBlank
    private String sourceUrl;

    @NotBlank
    private String imageUrl;

    // getters and setters
}

7. Взаимодействие с Browserless через FeignClient


Browserless предоставляет HTTP API, для взаимодействия с которым необходимо сгенерировать клиент с использованием FeignCleint.


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


В данном случае достаточно аннотации PostMapping, которая означает отправку POST-запроса по заданному пути (хост и порт получаются из Consul автоматически), и аннотации RequestBody, которая означает, что параметр метода будет значением тела запроса. Тип возвращаемого значения метода представляет собой класс, в объект которого отобразится тело ответа. По умолчанию тело запроса и ответа содержит JSON строку.


BlowserlessClient.java
@FeignClient("browserless")
public interface BrowserlessClient {
    @PostMapping("/function")
    LargestImageResponse findLargestImage(@RequestBody LargestImageRequest request);

    class LargestImageResponse {
        private String url;

        // getters and setters
    }

    class LargestImageRequest {
        private String code;

        private LargestImageRequestPayload context;

        // constructor, getters and setters
    }

    class LargestImageRequestPayload {
        private String url;

        // constructor, getters and setters
    }
}

В ресурсы необходимо положить JS-скрипт, который вытаскивает наибольшее изображение со страницы. Содержимое скрипта будет передаваться в Browserless в каждом запросе, как LargestImageRequest.code.


getLargestImage.js

Внимание: Объяснение содержимого скрипта находится вне рамок данной статьи.


module.exports = async ({page, context}) => {
    const {url} = context;
    await page.goto(url);
    await page.evaluate(_ => {
        window.scrollBy(0, window.innerHeight);
    });

    const data = await page._client.send('Page.getResourceTree')
        .then(tree => {
            return Array.from(tree.frameTree.resources)
                .filter(resource => resource.type === 'Image' && resource.url && resource.url.indexOf('.svg') == -1)
                .sort((a, b) => b.contentSize - a.contentSize)[0];
        });

    return {
        data,
        type: 'json'
    };
};

8. Контроллер и сервис


Сервис предоставляет метод, в котором происходит взаимодействие с репозиторием изображений и клиентом Browserless при запросе от клиента.


ImageService.java
@Service
public class ImageService {
    @Autowired
    private ImageRepository imageRepository;

    @Autowired
    private BrowserlessClient browserlessClient;

    private String getLargestImageScript;

    @PostConstruct
    public void initialize() throws IOException {
        getLargestImageScript = IOUtils.toString(getClass().getResourceAsStream("/getLargestImage.js"), StandardCharsets.UTF_8.name());
    }

    public Image findLargestImage(String url) {
        var largestImageResponse = browserlessClient.findLargestImage(
                new LargestImageRequest(
                        getLargestImageScript,
                        new LargestImageRequestPayload(url)));

        var image = new Image();

        image.setSourceUrl(url);
        image.setImageUrl(largestImageResponse.getUrl());

        return imageRepository.save(image);
    }
}

Контроллер предоставляет HTTP точку для запроса на получение картинки по ссылке на страницу.


ImageController.java
@RestController
public class ImageController {
    @Autowired
    private ImageService imageService;

    @GetMapping("/largest-image")
    public ResponseEntity<Image> getTitle(@RequestParam("url") String url) {
        return ResponseEntity.ok(imageService.findLargestImage(url));
    }
}

9. Заключение


Для тестирования работы приложения можно отправить GET-запрос по адресу http://localhost:8888/largest-image?url=https://maximka777.github.io/mb:


example of request and response


В итоге:


  • необходимые сервисы загружаются через Docker Compose и регистрируются в Consul для их последующего обнаружения;
  • конфигурация приложения загружается в key-value хранилище Consul;
  • Makefile содержит все команды по управлению проектом;
  • в приложении конфигурация для подключения к PostgreSQL загружается из key-value хранилища Consul, также из Consul подтягиваются данные о PostgreSQL сервисе;
  • FeignClient позволяет сгенерировать клиент для работы с Browserless.

Спасибо за внимание!


Весь код к проекту можно найти здесь.

Реклама
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее

Комментарии 5

    0

    Большое спасибо за подробный пример!

      0

      Раскажите, пожалуйста, на кого расчитана ваша статья? Как мне видится, максимальную пользу из неё извлечёт человек, который:


      1. Знает, что такое Docker и docker-compose (потому что никаких пояснений по формату compose.yml нет).
      2. Знает, что такое service registry и service discovery, и что Consul нужен именно для этого.
      3. Знает, что такое Makefile, но при этом ещё и является Java-программистом.
      4. Работает исключительно под Linux либо очень грамотно настроил констоль с bash и make под Windows.
      5. Умеет работать с Maven, Spring Boot, Spring Data, Spring Cloud, Browserless и Feign.
      6. Знает современный JavaScript.
      7. Обладает достаточными знаниями во всех вышеперечисленных областях, чтобы по обрывкам кода и конфигурационных файлов собрать рабочий проект.

      Это я к чему… Пояснений много не бывает. Ваша статья была бы гораздо полезнее, если бы вы хотя бы в двух словах объясняли, зачем нужна каждая из использованных технологий, приводили ссылки на документацию и выложили бы полный код проекта на GitHub.

        0
        Данная статья лишь показывает, как можно сделать и с использованием каких инструментов. И может считаться отправной точкой для изучения перечисленных инструментов.
        Да, согласен с тем, что пояснений маловато. Я за это извиняюсь :)
        Возможно вскоре доделаю некоторые моменты.
        Спасибо за отзыв!
        0
        Зачем так сложно? Spring Cloud поддерживает Consul как Configuration Service:
        bootstrap.yml
        spring:
          cloud:
            consul:
              host: ${CONSUL_HOST}
              port: ${CONSUL_PORT}
              config:
                prefix: configuration
                format: YAML
        

        А в самой аппликации @SpringCloudApplication. Теперь можно смело использовать конфигурацию из Consul.

        Помимо прочего Consul также является Service Discovery, Event Bus (можно обновлять аппликации без рестарта).

        Spring Cloud Consul
          0
          Согласен

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

        Самое читаемое