Построение микросервисной архитектуры на Golang и gRPC, часть 2 (docker) +11


Пришло время заняться контейнерами


Прежде всего, мы используем новейший образ Linux Alpine. Linux Alpine — это легкий дистрибутив Linux, разработанный и оптимизированный для запуска веб-приложений в Docker. Другими словами, Linux Alpine обладает достаточным количеством зависимостей и функциональных возможностей для выполнения большинства приложений. Это означает, что размер образа составляет около 8 МБ!

По сравнению с, скажем… виртуальной машиной Ubuntu объемом около 1 ГБ, вот почему образы Docker стали более естественным образом подходить для микросервисов и облачных вычислений.

Итак, теперь я надеюсь, что вы видите ценность в контейнеризации, и мы можем начать «Dockerising» нашего первого сервиса. Давайте создадим Dockerfile $ touch consignment-service/Dockerfile.



Первая часть
Оригинальный репозиторий EwanValentine
Оригинальна статья

В Dockerfile добавьте следующее:

FROM alpine:latest

RUN mkdir /app
WORKDIR /app
ADD consignment-service /app/consignment-service

CMD ["./consignment-service"]

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

Теперь давайте обновим запись сборки нашего Makefile, чтобы создать образ Docker.

build:
    ... 
    GOOS=linux GOARCH=amd64 go build
    docker build -t consignment .

Мы добавили еще два шага, и я хотел бы объяснить их немного подробнее. Прежде всего, мы создаем наш двоичный файл Go. Однако вы заметите две переменные окружения, прежде чем мы запустим $ go build. GOOS и GOARCH позволяют вам кросс-компилировать ваш бинарный файл для другой операционной системы. Так как я занимаюсь разработкой для Macbook, я не могу скомпилировать исполняемый файл go, а затем запустить его в контейнере Docker, который использует Linux. Двоичный файл будет полностью бессмысленным в вашем контейнере Docker, и он выдаст ошибку.

Второй шаг, который я добавил, — это процесс сборки докера. Docker прочтет ваш Dockerfile и создаст образ c именем consignment-service, точка обозначает путь к каталогу, поэтому здесь мы просто хотим, чтобы процесс сборки смотрел в текущий каталог.

Я собираюсь добавить новую запись в наш Makefile:

run: 
    docker run -p 50051:50051 shippy-service-consignment

Здесь мы запускаем наш образ докера, открывая порт 50051. Поскольку Docker работает на отдельном сетевом уровне, вам необходимо перенаправить порт. Например, если вы хотите запустить эту службу на порту 8080, вы должны изменить аргумент -p на 8080:50051. Вы также можете запустить контейнер в фоновом режиме, включив флаг -d. Например, docker run -d -p 50051:50051 consignment-service.

Запустите $ make run, затем в отдельной панели терминала снова $ go run main.go и проверьте, что он по-прежнему работает.

Когда вы запускаете $ docker build, вы встраиваете свой код и среду выполнения в образ. Образы Docker — это переносимые снимки вашей среды, и её зависимостей. Вы можете поделиться Docker images, опубликовав их в Docker Hub. Что похоже на npm или репозиторий yum для образов докеров. Когда вы определяете FROM в своем Dockerfile, вы говорите Docker вытянуть это изображение из хранилища Docker для использования в качестве базы. Затем вы можете расширить и переопределить части этого базового файла, переопределив их по своему усмотрению. Мы не будем публиковать наши образы докеров, но не стесняйтесь просматривать хранилище докеров и отмечать, что практически любое программное обеспечение уже было упаковано в контейнеры. Некоторые действительно замечательные вещи были Dockerised.

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

Достаточно о контейнерах! Давайте вернемся к нашему коду.

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

Именно здесь в игру вступает служба обнаружения. Служба обнаружения обновляет каталог всех ваших служб и их местоположения. Каждый сервис регистрируется во время выполнения и отменяет регистрацию при закрытии. Каждой службе затем присваивается имя или идентификатор. Таким образом, даже если у него может быть новый IP-адрес или адрес хоста, при условии, что имя службы остается прежним, вам не нужно обновлять вызовы этой службы из других служб.

Как правило, есть много подходов к этой проблеме, но, как и большинство вещей в программировании, если кто-то уже занимался этой проблемой, нет смысла заново изобретать колесо. @Chuhnk (Asim Aslam), создатель Go-micro, решает эти проблемы с фантастической ясностью и простотой использования. Он единолично производит фантастическое программное обеспечение. Пожалуйста, рассмотрите возможность помочь ему, если вам нравится то, что вы видите!

Go-micro


Go-micro — это мощный микросервисный фреймворк, написанный на Go, для использования, по большей части, с Go. Однако вы можете использовать Sidecar для взаимодействия с другими языками.

Go-micro имеет полезные функции для создания микросервисов в Go. Но мы начнем с, пожалуй, самой распространенной проблемы, которую он решает, и это обнаружение службы.

Нам нужно будет сделать несколько обновлений нашего сервиса, чтобы работать с go-micro. Go-micro интегрируется как плагин Protoc, в данном случае заменяя стандартный плагин gRPC, который мы используем в настоящее время. Итак, начнем с замены этого в нашем Makefile.

Обязательно установите зависимости go-micro:

go get -u github.com/micro/protobuf/{proto,protoc-gen-go}

Обновим наш Makefile для использования плагина go-micro вместо плагина gRPC:

build: 
        protoc -I. --go_out=plugins=micro:. \ proto/consignment/consignment.proto
        GOOS=linux GOARCH=amd64 go build
        docker build -t consignment .
run: 
    docker run -p 50051:50051 shippy-service-consignment

Теперь нам нужно обновить наш shippy-service-consignment/main.go для использования go-micro. Это абстрагирует большую часть нашего предыдущего кода gRPC. Он легко обрабатывает регистрацию и ускоряет написание сервиса.

shippy-service-consignment/main.go
// shippy-service-consignment/main.go
package main

import (

	"fmt"
    
    //Импортируем основной protobuf код
	pb "github.com/EwanValentine/shippy/consignment-service/proto/consignment"
	"github.com/micro/go-micro"
	"context"
)
//repository - интерфейс хранилища
type repository interface {
	Create(*pb.Consignment) (*pb.Consignment, error)
	GetAll() []*pb.Consignment
}

// Repository - структура для эмитации хранилища,
// после мы заменим её настоящим хранилищем
type Repository struct {
	consignments []*pb.Consignment
}

func (repo *Repository) Create(consignment *pb.Consignment) (*pb.Consignment, error) {
	updated := append(repo.consignments, consignment)
	repo.consignments = updated
	return consignment, nil
}

func (repo *Repository) GetAll() []*pb.Consignment {
	return repo.consignments
}

// Служба должна реализовать все методы для удовлетворения сервиса
// которые мы определили в нашем определении proto.
// Вы можете проверить интерфейсы в сгенерированном коде для точных сигнатур методов.
type service struct {
	repo repository
}

// CreateConsignment - мы создали только один метод для нашего сервиса,
// который является методом create, который принимает контекст и запрос
// потом они обрабатываются сервером gRPC.
func (s *service) CreateConsignment(ctx context.Context, req *pb.Consignment, res *pb.Response) error {

	// Save our consignment
	consignment, err := s.repo.Create(req)
	if err != nil {
		return err
	}

	// Return matching the `Response` message we created in our
	// protobuf definition.
	res.Created = true
	res.Consignment = consignment
	return nil
}

//GetConsignments - метод для получения всех партий из ответа сервера
func (s *service) GetConsignments(ctx context.Context, req *pb.GetRequest, res *pb.Response) error {
	consignments := s.repo.GetAll()
	res.Consignments = consignments
	return nil
}

func main() {

	repo := &Repository{}

	// Регистрируем новый сервис через Go-micro
	srv := micro.NewService(

		// Это имя должно совпадать с именем пакета объявленного в файле proto
		micro.Name("shippy.service.consignment"),
	)

	// Init will parse the command line flags.
	srv.Init()

	// Регистрация обработчитка
	pb.RegisterShippingServiceHandler(srv.Server(), &service{repo})

	// Запуск сервера
        log.Println("Запуск сервера")
	if err := srv.Run(); err != nil {
		fmt.Println(err)
	}
}


Основные изменения здесь — это способ, которым мы создаем наш сервер gRPC, который был аккуратно абстрагирован от mico.NewService(), который обрабатывает регистрацию нашего сервиса. И, наконец, функция service.Run(), которая обрабатывает само соединение. Как и прежде, мы регистрируем нашу реализацию, но на этот раз немного другим методом.

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

Наконец, мы больше не программируем порт. Go-micro должен быть настроен с использованием переменных среды или аргументов командной строки. Чтобы установить адрес, используйте MICRO_SERVER_ADDRESS=:50051. По умолчанию Micro использует mdns (multicast dns) в качестве посредника обнаружения служб для локального использования. Обычно вы не используете mdns для обнаружения сервисов в производственной среде, но мы хотим избежать необходимости запускать что-то вроде Consul или etcd локально для тестирования. Подробнее об этом позже.

Давайте обновим наш Makefile, чтобы отразить это.

build: 
        protoc -I. --go_out=plugins=micro:. \ proto/consignment/consignment.proto
        GOOS=linux GOARCH=amd64 go build
        docker build -t consignment .
run: 
     docker run -p 50051:50051 \ -e MICRO_SERVER_ADDRESS=:50051 \ shippy-service-consignment 

-e — флаг переменной среды, он позволяет передавать переменные среды в ваш контейнер Docker. Вы должны иметь флаг для каждой переменной, например -e ENV=staging -e DB_HOST=localhost и т. Д.

Теперь, если вы запустите $ make run, у вас будет сервис Dockerised с обнаружением сервиса. Итак, давайте обновим наш инструмент Cli, чтобы это использовать.

consignment-cli
package main

import (
	"encoding/json"
	"io/ioutil"
	"log"
	"os"

	"context"

	pb "github.com/EwanValentine/shippy-service-consignment/proto/consignment"

	micro "github.com/micro/go-micro"
)

const (
	address         = "localhost:50051"
	defaultFilename = "consignment.json"
)

func parseFile(file string) (*pb.Consignment, error) {
	var consignment *pb.Consignment
	data, err := ioutil.ReadFile(file)
	if err != nil {
		return nil, err
	}
	json.Unmarshal(data, &consignment)
	return consignment, err
}

func main() {
	service := micro.NewService(micro.Name("shippy.cli.consignment"))
	service.Init()

	client := pb.NewShippingServiceClient("shippy.service.consignment", service.Client())

	// Contact the server and print out its response.
	file := defaultFilename
	if len(os.Args) > 1 {
		file = os.Args[1]
	}

	consignment, err := parseFile(file)

	if err != nil {
		log.Fatalf("Could not parse file: %v", err)
	}

	r, err := client.CreateConsignment(context.Background(), consignment)
	if err != nil {
		log.Fatalf("Could not greet: %v", err)
	}
	log.Printf("Created: %t", r.Created)

	getAll, err := client.GetConsignments(context.Background(), &pb.GetRequest{})
	if err != nil {
		log.Fatalf("Could not list consignments: %v", err)
	}
	for _, v := range getAll.Consignments {
		log.Println(v)
	}
}


Здесь мы импортировали библиотеки go-micro для создания клиентов и заменили существующий код подключения на код клиента go-micro, который использует разрешение службы вместо прямого подключения к адресу.

Однако, если вы это запустите, работать не будет. Это потому, что мы сейчас запускаем наш сервис в контейнере Docker, который имеет свои собственные mdns, отдельно от mdns хоста, который мы используем в настоящее время. Самый простой способ исправить это — убедиться, что и служба, и клиент работают в «dockerland», так что они оба работают на одном хосте и используют один и тот же сетевой уровень. Итак, давайте создадим make consignment-cli/Makefile и создадим несколько записей.

build:
	GOOS=linux GOARCH=amd64 go build
	docker build -t shippy-cli-consignment .

run:
	docker run shippy-cli-consignment

Как и прежде, мы хотим собрать наш бинарный файл для Linux. Когда мы запускаем наш образ докера, мы хотим передать переменную окружения, чтобы дать команду go-micro использовать mdns.

Теперь давайте создадим Dockerfile для нашего инструмента CLI:

FROM alpine:latest

RUN mkdir -p /app
WORKDIR /app

ADD consignment.json /app/consignment.json
ADD consignment-cli /app/consignment-cli

CMD ["./shippy-cli-consignment"]

Это очень похоже на наш сервисный Dockerfile, за исключением того, что он также извлекает наш файл данных json.

Теперь, когда вы запускаете $ make run в вашем shippy-cli-consignment, вы должны увидеть Created: true, так же, как и раньше.

Теперь, похоже, самое время взглянуть на новую функцию Docker: многоэтапные сборки. Это позволяет нам использовать несколько образов Docker в одном Dockerfile.

Это особенно полезно в нашем случае, поскольку мы можем использовать одно изображение для создания нашего двоичного файла со всеми правильными зависимостями. А затем использовать второе изображение для его запуска. Давайте попробуем это, я оставлю подробные комментарии вместе с кодом:
consignment-service/Dockerfile
# consignment-service/Dockerfile

# Мы используем официальное изображение golang, которое содержит все 
# правильные инструменты сборки и библиотеки. Обратите внимание на `as builder`, 
# это дает этому контейнеру имя, на которое мы можем ссылаться позже.
FROM golang:alpine as builder

RUN apk --no-cache add git

# Установит рабочий каталог на наш текущий сервис в gopath
WORKDIR /app/shippy-service-consignment

# Скопирует текущий код в рабочий каталог
COPY . .

RUN go mod download

# Создаст двоичный файл с флагами, который позволит
# нам запустить этот двоичный файл в Alpine.
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o shippy-service-consignment

# Здесь мы используем второй оператор FROM, 
# это говорит Docker начать новый процесс сборки с этим же образом.
FROM alpine:latest

# Пакет, связанный с безопасностью - хорошо бы его иметь
RUN apk --no-cache add ca-certificates

# Как и прежде, создайте каталог для нашего приложения.
RUN mkdir /app
WORKDIR /app

# Здесь вместо того, чтобы копировать двоичный файл с нашего хоста,
# мы извлекаем двоичный файл из контейнера с именем `builder`
# Это позволяет заглянуть в наш предыдущий образ, 
# и найти двоичный файл, который мы создали ранее,
# и поместить его в этот контейнер. Удивительно!
COPY --from=builder /app/shippy-service-consignment/shippy-service-consignment .

# Запустит бинарный файл как обычно! На этот раз с бинарной сборкой в
# отдельном контейнере со всеми правильными зависимостями и
# run time библиотеками.
CMD ["./shippy-service-consignment"]


Теперь я перейду к другим файлам Docker и применю этот новый подход. О, и не забудьте удалить $ go build из ваших Makefiles!

Судовое обслуживание


Давайте создадим второй сервис. У нас есть служба (shippy-service-consignment), которая занимается согласованием партии контейнеров с судном, которое лучше всего подходит для этой партии. Чтобы соответствовать нашей партии, мы должны отправить вес и количество контейнеров в наш новый судовой сервис, который затем найдет судно, способное обработать эту партию.

Создайте новый каталог в вашем корневом каталоге $ mkdir vessel-service, теперь создайте подкаталог для нашего нового определения protobuf служб, $ mkdir -p shippy-service-vessel/proto/vessel. Теперь давайте создадим новый файл protobuf, $ touch shippy-service-vessel/proto/vessel/vessel.proto.

Поскольку определение protobuf действительно является ядром нашего программного дизайна, давайте начнем с него.

vessel/vessel.proto
// shippy-service-vessel/proto/vessel/vessel.proto
syntax = "proto3";

package vessel;

service VesselService {
  rpc FindAvailable(Specification) returns (Response) {}
}

message Vessel {
  string id = 1;
  int32 capacity = 2;
  int32 max_weight = 3;
  string name = 4;
  bool available = 5;
  string owner_id = 6;
}

message Specification {
  int32 capacity = 1;
  int32 max_weight = 2;
}

message Response {
  Vessel vessel = 1;
  repeated Vessel vessels = 2;
}


Как видите, это очень похоже на наш первый сервис. Мы создаем сервис с одним методом rpc под названием FindAvailable. Это принимает тип Specification и возвращает тип Response. Тип Response возвращает либо тип Vessel либо несколько судов, используя повторяющееся поле.

Теперь нам нужно создать Makefile для обработки нашей логики сборки и нашего скрипта запуска. $ touch shippy-service-vessel/Makefile. Откройте этот файл и добавьте следующее:

// vessel-service/Makefile
build:
	protoc -I. --go_out=plugins=micro:.       proto/vessel/vessel.proto
	docker build -t shippy-service-vessel .

run:
	docker run -p 50052:50051 -e MICRO_SERVER_ADDRESS=:50051 shippy-service-vessel

Это почти идентично первому Makefile, который мы создали для нашего consignment сервиса, однако обратите внимание, что названия сервисов и порты немного изменились. Мы не можем запустить два док-контейнера на одном и том же порту, поэтому используем переадресацию портов Dockers, чтобы эта служба перенаправляла от 50051 до 50052 в сети хоста.

Теперь нам нужен Dockerfile, использующий наш новый многоэтапный формат:

# vessel-service/Dockerfile
FROM golang:alpine as builder

RUN apk --no-cache add git

WORKDIR /app/shippy-service-vessel

COPY . .

RUN go mod download
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o shippy-service-vessel

FROM alpine:latest

RUN apk --no-cache add ca-certificates

RUN mkdir /app
WORKDIR /app
COPY --from=builder /app/shippy-service-vessel .

CMD ["./shippy-service-vessel"]

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

vessel-service/main.go
// vessel-service/main.go
package main

import (
	"context"
	"errors"
	"fmt"

	pb "github.com/EwanValentine/shippy/vessel-service/proto/vessel"
	"github.com/micro/go-micro"
)

type Repository interface {
	FindAvailable(*pb.Specification) (*pb.Vessel, error)
}

type VesselRepository struct {
	vessels []*pb.Vessel
}

// FindAvailable - проверяет спецификацию по карте судов,
// если вместимость и максимальный вес ниже вместимости судна и максимального веса,
// тогда возвращаем это судно в ответ.
func (repo *VesselRepository) FindAvailable(spec *pb.Specification) (*pb.Vessel, error) {
	for _, vessel := range repo.vessels {
		if spec.Capacity <= vessel.Capacity && spec.MaxWeight <= vessel.MaxWeight {
			return vessel, nil
		}
	}
        //если не найдём нужного судна
	return nil, errors.New("судов с задаными параметрами не найдено")
}

// Наш обработчик сервиса grpc
type service struct {
	repo repository
}

func (s *service) FindAvailable(ctx context.Context, req *pb.Specification, res *pb.Response) error {

	// Поиск следующего подходящего судна
	vessel, err := s.repo.FindAvailable(req)
	if err != nil {
		return err
	}

	// Определяем ответ с заполненым полем судна
	res.Vessel = vessel
	return nil
}

func main() {
	vessels := []*pb.Vessel{
		&pb.Vessel{Id: "vessel001", Name: "Boaty McBoatface", MaxWeight: 200000, Capacity: 500},
	}
	repo := &VesselRepository{vessels}

	srv := micro.NewService(
		micro.Name("shippy.service.vessel"),
	)

	srv.Init()

	// Регистрация нашего сервиса
	pb.RegisterVesselServiceHandler(srv.Server(), &service{repo})

	if err := srv.Run(); err != nil {
		fmt.Println(err)
	}
}


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

shippy/consignment-service/main.go
package main

import (
	"context"
	"fmt"
	"log"
	"sync"

	pb "github.com/EwanValentine/shippy-service-consignment/proto/consignment"
	vesselProto "github.com/EwanValentine/shippy-service-vessel/proto/vessel"
	"github.com/micro/go-micro"
)

const (
	port = ":50051"
)

type repository interface {
	Create(*pb.Consignment) (*pb.Consignment, error)
	GetAll() []*pb.Consignment
}

// Repository - структура для эмитации хранилища,
// после мы заменим её настоящим хранилищем
type Repository struct {
	mu           sync.RWMutex
	consignments []*pb.Consignment
}

//Create - создаём новую партию груза
func (repo *Repository) Create(consignment *pb.Consignment) (*pb.Consignment, error) {
	repo.mu.Lock()
	updated := append(repo.consignments, consignment)
	repo.consignments = updated
	repo.mu.Unlock()
	return consignment, nil
}

//GetAll - метод получения всех партий из хранилища
func (repo *Repository) GetAll() []*pb.Consignment {
	return repo.consignments
}

// Служба должна реализовать все методы для удовлетворения сервиса
// которые мы определили в нашем определении proto. 
// Вы можете проверить интерфейсы в сгенерированном коде для точных сигнатур методов
type service struct {
	repo repository
	vesselClient vesselProto.VesselServiceClient
}

// CreateConsignment - мы создали только один метод для нашего сервиса create,
// который принимает контекст и запрос, после он обрабатывается сервером gRPC.
func (s *service) CreateConsignment(ctx context.Context, req *pb.Consignment, res *pb.Response) error {

	// Здесь мы опледеляем судно исходя из веса нашего груза,
       // и количества контейнеров
	vesselResponse, err := s.vesselClient.FindAvailable(context.Background(), &vesselProto.Specification{
		MaxWeight: req.Weight,
		Capacity: int32(len(req.Containers)),
	})
	log.Printf("Судно найдено: %s \n", vesselResponse.Vessel.Name)
	if err != nil {
		return err
	}

	// В ответ мы передадим id судна
	req.VesselId = vesselResponse.Vessel.Id


	// Сохраним партию груза в репозиторий
	consignment, err := s.repo.Create(req)
	if err != nil {
		return err
	}

	res.Created = true
	res.Consignment = consignment
	return nil
}

// GetConsignments - метод для получения всех партий из ответа сервера
func (s *service) GetConsignments(ctx context.Context, req *pb.GetRequest, res *pb.Response) error {
	consignments := s.repo.GetAll()
	res.Consignments = consignments
	return nil
}

func main() {
        //Создание пустого хранилища
	repo := &Repository{}

	//Создание экземпляра micro
	srv := micro.NewService(
		micro.Name("shippy.service.consignment"),
	)

	srv.Init()

	vesselClient := vesselProto.NewVesselServiceClient("shippy.service.vessel", srv.Client())

	// Регистрация службы ответов на сервере gRPC.
	pb.RegisterShippingServiceHandler(srv.Server(), &service{repo, vesselClient})

	// Запуск сервера
	if err := srv.Run(); err != nil {
		fmt.Println(err)
	}
}


Здесь мы создали клиентский экземпляр для нашего сервиса судна, что позволяет нам использовать имя сервиса, т.е. shipy.service.vessel для вызова сервиса судна в качестве клиента и взаимодействия с его методами. В этом случае только один метод ( FindAvailable ). Мы отправляем вес партии вместе с количеством контейнеров, которые мы хотим отправить, в качестве спецификации для службы судна. Которая возвращает нам соответствующее этой спецификации судно.

Обновите файл consignment-cli / consignment.json, удалите жестко закодированный ship_id, ведь мы хотим подтвердить, что наша служба поиска судна работает. Так же давайте добавим еще несколько контейнеров и увеличим вес. Например:

{
  "description": "Тестовая партия груза",
  "weight": 55000,
  "containers": [
    {
      "customer_id": "Заказчик_001",
      "user_id": "Пользователь_001",
      "origin": "Ростов-на-Дону"
    },
    {
      "customer_id": "Заказчик_002",
      "user_id": "Пользователь_001",
      "origin": "Новоросийск"
    },
    {
      "customer_id": "Заказчик_003",
      "user_id": "Пользователь_001",
      "origin": "Туапсе"
    }
  ]
}

Теперь запустите $ make build && make run в consignment-cli. Вы должны увидеть ответ со списком созданных грузов. В ваших партиях вы должны увидеть, что установлен параметр vessel_id.

Итак, у нас есть два взаимосвязанных микросервиса и интерфейс командной строки!
В следующей части этой серии мы рассмотрим сохранение некоторых из этих данных с использованием MongoDB. Мы также добавим третий сервис и используем docker-compose для локального управления нашей растущей экосистемой контейнеров.

Первая часть
Оригинальный репозиторий EwanValentine




К сожалению, не доступен сервер mySQL