Статьи

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

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

Мы сделаем сервис книг. Это должно быть в состоянии сделать следующее:

  • Список всех книг
  • Получить всю информацию, связанную с книгой
  • Обновить существующую книгу
  • Удалить существующую книгу

Эта статья не будет пытаться научить всему, что нужно знать о Scala, Spray, Akka, MongoDB, Docker, Vagrant, Ansible, TDD и т. Д. Нет единой статьи, которая могла бы сделать это. Цель состоит в том, чтобы показать поток и настройку, которую можно использовать при разработке сервисов. На самом деле, большая часть этой статьи одинаково актуальна для других типов разработок. Docker имеет гораздо более широкое использование, чем микросервисы, Ansible и CM в целом могут использоваться для любого типа предоставления, а Vagrant очень полезен для быстрого создания виртуальных машин.

Среда

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

Давайте начнем с клонирования репо.

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

Далее мы создадим сервер Ubuntu, используя Vagrant. Определение следующее:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
# -*- 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

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

Предпочтительным способом работы с Ansible является разделение конфигураций на роли. В нашем случае в каталоге ansible / role есть четыре роли. Один будет убедиться, что Scala и SBT установлены, другой — что Docker запущен и работает, а другой запустит контейнер MongoDB. Последняя роль (книги) будет использована позже для развертывания службы, которую мы создаем, на производственную виртуальную машину.

Как пример, определение роли mongodb следующее.

01
02
03
04
05
06
07
08
09
10
11
12
13
- 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]

Это должно быть самоочевидно для тех, кто работал с Docker. Роль гарантирует, что каталог присутствует и что контейнер mongodb запущен. Playbook ansible / dev.yml — это то, где мы все связываем.

1
2
3
4
5
6
7
- hosts: localhost
  remote_user: vagrant
  sudo: yes
  roles:
    - scala
    - docker
    - mongodb

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

Хорошая особенность Ansible и Configuration Management в целом заключается в том, что они не запускают сценарии вслепую, а действуют только при необходимости. Если вы запустите инициализацию во второй раз, Ansible обнаружит, что все в порядке, и ничего не сделает. С другой стороны, если, например, вы удалите каталог / data / db , Ansible обнаружит, что он отсутствует, и создаст его заново.

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

1
2
3
vagrant up dev
vagrant ssh dev
ll /vagrant

vagrant up создает новую виртуальную машину или оживляет существующую. С помощью vagrant ssh мы можем войти во вновь созданную коробку. Наконец, ll / vagrant перечисляет все файлы в этом каталоге в качестве доказательства того, что все наши локальные файлы доступны внутри виртуальной машины.

Вот и все. Наша среда разработки с контейнерами Scala, SBT и MongoDB готова. Теперь пришло время развивать наш книжный сервис.

Книжный Сервис

Я люблю Скалу и Акку . Scala — очень мощный язык, а Akka — мой любимый фреймворк для создания приложений JVM, управляемых сообщениями. Несмотря на то, что Akka была рождена в Scala, она также может использоваться с Java.

Spray — это простой, но очень мощный инструментарий для создания приложений на основе REST / HTTP. Он асинхронный, использует акторы Akka и имеет отличный (хотя и странный в начале) DSL для определения HTTP-маршрутов.

В стиле TDD мы проводим тесты перед внедрением. Вот пример тестов для маршрута, который извлекает список всех книг.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
"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 .

Как бы мы реализовали эти тесты? Вот код, который обеспечивает реализацию на основе приведенных выше тестов.

1
2
3
4
5
6
7
val route = pathPrefix("api" / "v1" / "books") {
  get {
    complete(
      collection.find().toList.map(grater[BookReduced].asObject(_))
    )
   }
}

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

И тесты, и реализация, представленные здесь, являются упрощенными, и в реальных сценариях было бы больше работы. На самом деле, сложные маршруты и сценарии — это то, где Spray действительно сияет.

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

[Внутри ВМ]

1
2
cd /vagrant
sbt ~test-quick

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

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

Как и любое другое приложение, оно должно быть протестировано, построено и развернуто.

Давайте создадим контейнер Docker с сервисом. Определение, необходимое для создания контейнера, можно найти в Dockerfile .

[Внутри ВМ]

1
2
3
4
cd /vagrant
sbt assembly
sudo docker build -t vfarcic/books-service .
sudo docker push vfarcic/books-service

Мы собираем JAR (тесты являются частью задачи сборки), собираем Docker-контейнер и помещаем его в Hub. Если вы планируете воспроизвести эти шаги, создайте учетную запись на hub.docker.com и измените vfarcic на свое имя пользователя.

Контейнер, который мы создали, содержит все необходимое для запуска этого сервиса. Он основан на Ubuntu, имеет JDK7, содержит экземпляр MongoDB и имеет JAR, который мы собрали. Отныне этот контейнер можно запускать на любой машине, на которой установлен Docker. Нет необходимости устанавливать JDK, MongoDB или любую другую зависимость на сервер. Контейнер самодостаточен и может работать где угодно.

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

Чтобы создать производственную виртуальную машину с развернутым сервисом книг, выполните следующее

[из исходного каталога]

1
2
vagrant halt dev
vagrant up prod

Первая команда останавливает разработку ВМ. Каждый требует 2 ГБ. Если у вас достаточно оперативной памяти, вам не нужно останавливать ее, и вы можете пропустить эту команду. Второй выводит производственную виртуальную машину с развернутым сервисом books.

После небольшого ожидания создается новая виртуальная машина, устанавливается Ansible и запускается playbook prod.yml . Он устанавливает Docker и запускает vfarcic / books-service, который был ранее собран и отправлен в Docker Hub. Во время работы у него будет открыт порт 8080, и он будет использовать каталог / data / db с хостом.

Давайте попробуем это. Сначала мы должны отправить запросы PUT, чтобы вставить некоторые тестовые данные.

1
2
3
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

Давайте проверим, возвращает ли сервис правильные данные.

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

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

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

Мы можем проверить, что удаленной книги больше нет.

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

Наконец, мы можем запросить конкретную книгу.

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

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

Резюме

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

Спрей — очень хороший выбор для микросервисов. Контейнеры Docker светятся, когда они содержат все, что нужно приложению, но не больше. Использование больших веб-серверов, таких как JBoss и WebSphere, было бы излишним для одной (небольшой) службы. Даже веб-серверы меньшего размера, такие как Tomcat, не нужны. Играть в! отлично подходит для создания RESTful API. Тем не менее, он по-прежнему содержит много вещей, которые нам не нужны. Спрей, с другой стороны, делает только одно и делает это хорошо. Он обеспечивает возможности асинхронной маршрутизации для API RESTful.

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

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