Разработка микросервисов с использованием Scala, Spray, MongoDB, Docker и Ansible

в 15:13, , рубрики: Ansible, api, docker, scala, vagrant, Веб-разработка, микросервисы, Программирование, разработка

Цель данной статьи — показать возможный подход для построения микросервисов с использованием Scala, RESTful JSON, Spray и Akka. В качестве базы данных мы будем использовать MongoDB. В результате нашей работы мы упакуем наш проект в Docker-контейнер, а Vagrant и Ansible позволит нам управлять конфигурацией приложения.

В этой статье вы не найдете подробностей о языке Scala и других технологиях, которые будут использоваться в проекте. В ней вы не найдете руководства, которое ответит на все ваши вопросы. Цель статьи — показать технику, которую можно использовать при разработке микросервисов. На самом деле, большая часть этой статьи не завязана на конкретной технологии. Docker имеет более широкую сферу использования, нежели только микросервисы. Ansible позволяют быстро развернуть любое требуемое окружение, а Vagrant — отличный инструмент для создания виртуальных машин.

Итак, приступим к созданию «Книжного сервиса» со следующими методами:

  • Получить все книги
  • Получить информацию о книге
  • Обновить существующую книгу
  • Удалить существующую книгу

Окружение

Мы будем использовать Ubuntu в качестве сервера. Наиболее простой путь его создать – воспользоваться Vagrant. Если у вас он еще не установлен, пожалуйста, скачайте его и установите. Вам так же потребуется Git для клонирования репозитория с исходным кодом. Остальная часть статьи не потребует от вас ручной установки дополнительных пакетов.

Приступим к клонированию репозитория

git clone https://github.com/vfarcic/books-service.git
cd books-service

Далее необходимо создать Ubuntu-сервер используя Vagrant со следующими настройками:

# -*- mode: ruby -*-
# vi: set ft=ruby :

VAGRANTFILE_API_VERSION = "2"
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
  config.vm.box = "ubuntu/trusty64"
  config.vm.synced_folder ".", "/vagrant"
  config.vm.provision "shell", path: "bootstrap.sh"
  config.vm.provider "virtualbox" do |v|
    v.memory = 2048
  end
  config.vm.define :dev do |dev|
    dev.vm.provision :shell, inline: 'ansible-playbook /vagrant/ansible/dev.yml -c local'
  end
  config.vm.define :prod do |prod|
    prod.vm.provision :shell, inline: 'ansible-playbook /vagrant/ansible/prod.yml -c local'
  end
end

Мы определили box (ОС) как Ubuntu, synced_folder означает, что всё, находящееся внутри каталога ./vagrant на хост-машине будет доступно внутри виртуальной машины. Остальную работу, по установке приложений и подготовки окружения мы возложим на Ansible, который будет установлен с помощью bootstrap.sh. В Vagrantfile находится две виртуальные машины: dev и prod. Каждая из них будет использовать Ansible, поэтому убедитесь, что он установлен корректно.

Классический путь работы с Ansible — разделить конфигурацию на роли. В нашем случае мы имеем 4 роли, находящиеся в ansible/roles. Первая роль включает в себя установку Scala и SBT. Еще одна устанавливает Docker. Третья устанавливает MongoDB-контейнер. Последняя роль (books) будет использоваться для разворачивания приложения на боевую виртуальную машину.
В качестве примера, объявим mongodb роль следующим образом:

- name: Directory is present
  file:
    path=/data/db
    state=directory
  tags: [mongodb]

- name: Container is running
  docker:
    name=mongodb
    image=dockerfile/mongodb
    ports=27017:27017
    volumes=/data/db:/data/db
  tags: [mongodb]

Эта роль гарантирует, что папка существует и контейнер mongodb работает. Плейбук ansible/dev.yml связывает эти роли вместе:

- hosts: localhost
  remote_user: vagrant
  sudo: yes
  roles:
    - scala
    - docker
    - mongodb

Каждый раз когда мы запускаем плейбук, выполняются все задачи из ролей scala, docker и mongodb.

Одна из прелестей Ansible в том, что он выполняет задачи только тогда, когда это нужно. Если вы запустите его во второй раз, он проверит, что всё на месте и ничего не сделает. Однако, если вы удалите папку /data/db, Ansible заметит пропажу и создаст её снова.

Время запускать виртуальную машину! Первый запуск, будет немного долгим, так как Vagrant необходимо скачать дистрибутив Ubuntu, установить пакеты и скачать Docker-образ MongoDB. Последующие запуски будут происходит заметно быстрее.

vagrant up dev
vagrant ssh dev
ll /vagrant

Команда vagrant up создает новую виртуальную машину или запускает одну из существующих. С vagrant ssh мы заходим на недавно созданную машину. И, наконец ll /vagrant показывает список файлов и директорий как доказательство того, что наши локальные файлы доступны внутри виртуальной машины.
Это всё. Наш сервер разработки со Scala, SBT и MongoDB готов к работе. Приступим к разработке нашего сервиса.

«Книжный сервис»

Мне нравится Scala, это очень мощный язык, а akka мой любимый фреймворк для построения message-driven JVM-приложений. Несмотря на то, что Akka появилась из Scala, ничто не мешает использовать её в Java.
Spray — это простой, но мощный инструмент для построения REST/HTTP сервисов. Он асинхронный, благодаря использованию Akka-акторов и имеет замечательный DSL для описания HTTP маршрутов.
В моду использования TDD мы пишем тесты перед реализацией. Вот пример тестов, который проверяет маршрут, по которому отдается список книг.

"GET /api/v1/books" should {
  "return OK" in {
    Get("/api/v1/books") ~> route ~> check {
      response.status must equalTo(OK)
    }
  }

  "return all books" in {
    val expected = insertBooks(3).map { book =>
      BookReduced(book._id, book.title, book.author)
    }

    Get("/api/v1/books") ~> route ~> check {
      response.entity must not equalTo None
      val books = responseAs[List[BookReduced]]
      books must haveSize(expected.size)
      books must equalTo(expected)
    }
  }
}

Это простой тест, который, надеюсь, покажет направление, в котором нужно двигаться для разработки тестов на API построенные на Spray. Первое, что мы проверяем — сервер, по данному запросу должен возвращать код 200 (ОК). Второе, на что мы обращаем внимание, что после добавления книги в базу данных, она корректно возвращается. Полный исходный код с тестами вы можете посмотреть в ServiceSpec.scala

Как реализованы эти проверки? Код, который это позволит, представлен ниже:

val route = pathPrefix("api" / "v1" / "books") {
  get {
    complete(
      collection.find().toList.map(grater[BookReduced].asObject(_))
    )
  }
}

Мы определили маршрут /api/v1/books, GET метод и ответ внутри выражения complete(). В нашем случае мы получили список всех книг из БД и преобразовали его к кейс-классу BookReduced. Весь исходный код, включающий все методы (GET, PUT, DELETE) вы можете найти в ServiceActor.scala
Оба теста и реализация приведены в качестве примера, на практике они, как правило, сложнее. Но Spray с этим справляется по-настоящему здорово.

Во время разработки вы можете запустить тесты в быстром режиме.

#[Внутри виртуальной машины]
cd /vagrant
sbt ~test-quick

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

Тестирование, сборка и развертывание

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

#[Внутри виртуальной машины]
cd /vagrant
sbt assembly
sudo docker build -t vfarcic/books-service .
sudo docker push vfarcic/books-service

Мы скомпилировали JAR (прохождение тестов — это часть этапа сборки), собрали Docker-контейнер и отправили это в Docker Hub. Если вы планируете воспроизвести эти шаги снова, пожалуйста, создайте аккаунт в hub.docker.com и измените «vfarcic» на ваш логин.
Контейнер, который мы создали содержит всё, что нужно для запуска нашего сервиса. Он основан на Ubuntu, содержит JDK7 с MongoDB и собранный JAR-файл. Этот контейнер может быть запущен на любой машине, с установленным Docker. Он не требует установки дополнительных зависимостей на сервер, контейнер самодостаточен и может быть запущен где угодно.
Давайте развернем (запустим) контейнер который мы создали на другой виртуальной машине. Это очень похоже на разворачивание приложение в production-среде.
Для создания production-виртуальной машины, с нашим сервисом необходимо выполнить следующие команды:

#[из директории с исходным кодом]
vagrant halt dev
vagrant up prod

Первая команда останавливает dev-виртуальную машину. Каждая машина требует 2ГБ ОЗУ, и если вы имеете достаточно свободной ОЗУ, то можете пропустить этот шаг. Вторая команда запускает production-машину, с развернутым сервисом.
Через некоторое время ожидания, виртуальная машина создастся, Ansible установится и запустит playbook prod.yml. Он установит Docker и запустит vfarcic/books-service, собранный на предыдущем шаге и отправленный в Docker Hub. Во время работы от будет использовать порт 8080 и иметь общую, с хостовой системой, папку /data/db.
Давайте попробуем, что у нас получилось. Для начала попробуем отправить PUT-запрос, чтобы добавить тестовые данные:

curl -H 'Content-Type: application/json' -X PUT -d '{"_id": 1, "title": "My First Book", "author": "John Doe", "description": "Not a very good book"}' http://localhost:8080/api/v1/books
curl -H 'Content-Type: application/json' -X PUT -d '{"_id": 2, "title": "My Second Book", "author": "John Doe", "description": "Not a bad as the first book"}' http://localhost:8080/api/v1/books
curl -H 'Content-Type: application/json' -X PUT -d '{"_id": 3, "title": "My Third Book", "author": "John Doe", "description": "Failed writers club"}' http://localhost:8080/api/v1/books

Давайте проверим, что сервис вернул нам корректные данные:

curl -H 'Content-Type: application/json' http://localhost:8080/api/v1/books

Мы можем удалить книгу:

curl -H 'Content-Type: application/json' -X DELETE http://localhost:8080/api/v1/books/_id/3

Проверим, что удаленная книга более не существует:

curl -H 'Content-Type: application/json' http://localhost:8080/api/v1/books

В заключение попробуем извлечь конкретный экземпляр книги:

curl -H 'Content-Type: application/json' http://localhost:8080/api/v1/books/_id/1

Мы попробовали быстрый способ разработки, сборки и разворачивания микросервиса. Docker упрощает разворачивание и не требует дополнительных зависимостей. Каждый сервис, требующий JDK и MongoDB не требует установленных приложений на конечной машине. Это всё является частью контейнера, который запущен как Docker-процесс.

Итоги

Идея микросервисов существовала долгое время, и до недавнего времени не получала должного внимания, из-за проблем совместимости приложений, необходимых для одновременной работы сотен и тысяч различных экземпляров сервисов. Преимущества, которые возникали благодаря использованию микросервисов (разделение, уменьшение срока разработки, масштабируемость и т.д) были не столь значимыми по сравнению с проблемами, которые они несли при обеспечении нужного окружения и разворачивании. Docker и такие инструменты как Ansible помогают значительно сократить усилия. С уходом от этой проблемы, самые разнородные микросервисы становятся в моде из-за преимуществ, которые они несут.

Spray — это отличный выбор для микросервиса. Docker-контейнеры содержат всё, что нужно для работы приложения, и ничего лишнего. Использование больших веб-серверов, таких как JBoss и WebSphere может быть неоправданно для небольшого сервиса. Даже в таких небольших серверах как Tomcat обычно нет необходимости. Play! — это замечательный фреймворк для построения RESTFul API, однако он содержит много вещей которые мы не используем. Spray, c другой стороны делает всего лишь одну вещь — предоставляет асинхронную маршрутизацию для RESTFul API, и делает это великолепно.

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

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

Автор: Mobyman

Источник

Поделиться

* - обязательные к заполнению поля