Skip to content

Latest commit

 

History

History
544 lines (418 loc) · 29.2 KB

part25-rus.md

File metadata and controls

544 lines (418 loc) · 29.2 KB

Как создать файл Docker-compose и управлять порядком запуска сервисов с помощью wait-for.sh

Оригинал

Всем привет! Рад вас снова видеть на мастер-классе по бэкенду!

На прошлой лекции мы узнали, как использовать Docker сеть, чтобы связать два отдельных контейнера по именам. Сегодня я покажу вам, как использовать docker-compose для автоматической настройки всех сервисов в одной и той же Docker сети и их одновременного запуска с помощью всего лишь одной команды.

Хорошо, давайте начнём!

Как создать файл Docker-compose

Во-первых, я создам новый файл: docker-compose.yaml в корне нашего проекта. Вы можете настроить множество параметров с помощью файла docker-compose. О всех них можно прочитать в документации, перейдя на docs.docker.com, открыв Reference, Compose file reference, version 3. В правом меню вы можете увидеть список различных синтаксисов.

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

По сути вы определяете версию docker-compose, а затем список сервисов, которые вы хотите запустить вместе. В этом примере определяется сервис redis, сервис для БД postgres и некоторые другие веб-сервисы для голосования и вычисления его результатов.

Пока я просто скопирую первые две строки этого примера и вставлю их в наш файл docker-compose.yaml. Обратите внимание, что yaml файл очень чувствителен к отступам.

version: "3.9"
services:

На данный момент в файле используется отступ в 4 пробела, но я хочу использовать для них 2 пробела. Поэтому я нажму на кнопку, показанную на рисунке

выберу Indent using spaces,

и 2 в качестве размера отступа.

Итак, если теперь мы нажмём Enter, то увидим, что отступ для новых строк равен 2 пробелам. Далее нам нужно объявить список сервисов, которые мы хотим запустить. Первым сервисом должна быть БД postgres. Для этого сервиса мы будем использовать предварительно собранный Docker образ. Поэтому пишем ключевое слово image, за которым следуют «название» и «тег» образа, в данном случае это postgres:12-alpine. Теперь мы будем использовать ключевое слово environment, чтобы указать некоторые переменные окружения для имени пользователя, пароля и названия базы данных, точно так же, как мы делали это в рабочем процессе GitHub CI.

  postgres:
    image: postgres:12-alpine
    environment:

Сначала скопируем переменную POSTGRES_USER. Синтаксис немного отличается, мы должны использовать оператор равенства для присвоения значения. Точно так же я скопирую переменную POSTGRES_PASSWORD и, наконец, переменную POSTGRES_DB из файла ci.yaml.

    environment:
      - POSTGRES_USER=root
      - POSTGRES_PASSWORD=secret
      - POSTGRES_DB=simple_bank

Хорошо, затем мы объявим сервис api, который будет обслуживать все запросы к API нашего простого банковского приложения. Для этого сервиса мы должны собрать его образ из Golang исходников, поэтому под ключевым словом build указываем context для создания образа:

api:
    build:
      context: .

Эта точка означает текущую корневой каталог. Затем мы используем ключевое слово dockerfile, чтобы сообщить docker-compose, где найти Docker файл для создания образа. В данном случае это просто Dockerfile в корне проекта. Затем мы должны открыть доступ к порту 8080 извне, для хост-машины, чтобы мы могли вызывать API нашего простого банковского приложения из-за пределов контейнера.

  api:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "8080:8080"

Одна из самых важных вещей, которые мы должны сделать, — это сообщить сервису api, как подключиться к сервису postgres. Для этого мы установим одну переменную среды: DB_SOURCE. Как мы видели в предыдущей лекции, установка этой переменной окружения переопределит значение, объявленное в файле app.env, и, поскольку все сервисы в этом файле docker-compose будут работать в одной сети, они могут взаимодействовать друг с другом по имени. Поэтому здесь, в этом URL DB_SOURCE=postgresql://root:secret@localhost:5432/simple_bank?sslmode=disable вместо localhost мы будем использовать название сервиса postgres.

api:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "8080:8080"
    environment:
      - DB_SOURCE=postgresql://root:secret@postgres:5432/simple_bank?sslmode=disable

И на этом в принципе всё! Файл docker-compose готов. Попробуем его запустить!

Если на вашем компьютере установлена последняя версия Docker CLI, всё, что вам нужно сделать, это запустить docker-compose up

docker-compose up

Затем Docker-compose автоматически найдет файл docker-compose.yaml в текущей папке и запустит его для вас. Как вы можете видеть здесь, перед запуском сервиса он должен сначала создать Docker образ для сервиса api нашего простого банковского приложения. Затем, после того, как образ будет успешно собран, Docker-compose запустит сразу как сервис postgres, так и api сервис.

В логах по префиксу строки видно, какой сервис его послал. В данном случае это либо postgres_1, либо api_1. Теперь, если в другой вкладке терминала мы запустим

docker images

то увидим новый образ simplebank_api.

Как видно, имя образа имеет префикс simplebank, что является названием папки, содержащей наш Docker файл. А его суффикс — это название самого сервиса, то есть api.

Теперь давайте запустим

docker ps 

чтобы увидеть все запущенные сервисы. Существует два сервиса: simple_bank_postgres_1 и simple_bank_api_1. Оба они имеют префикс simple_bank за которым идёт название сервиса.

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

Теперь если мы проинспектируем сеть simplebank_default, то увидим, что два контейнера с сервисами на самом деле работают в этой же сети.

Вот почему они могут обнаруживать друг друга по именам.

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

Ой, мы получили 500 Internal Server Error и причина в следующем: "relation users does not exist" ("отношения (таблицы) users не существует"). Знаете почему?

Обновляем Docker образ

Потому что мы еще не выполнили миграцию для создания схемы базы данных. Для этого мы должны обновить наш Docker образ, чтобы запустить миграцию базы данных перед запуском API сервера. Мы сделаем это по аналогии с тем, как мы это делали в рабочем процессе GitHub CI: нам нужно будет загрузить двоичный файл golang-migrate в Docker образ и использовать его для запуска миграции.

Итак, я скопирую эти 2 инструкции и вставлю их в Dockerfile на этапе сборки.

curl -L https://github.com/golang-migrate/migrate/releases/download/v4.14.1/migrate.linux-amd64.tar.gz | tar xvz
sudo mv migrate.linux-amd64 /usr/bin/migrate

Сначала мы должны запустить эту curl команду

curl -L https://github.com/golang-migrate/migrate/releases/download/v4.14.1/migrate.linux-amd64.tar.gz | tar xvz      

для загрузки и распаковки двоичного файла migrate.

RUN curl -L https://github.com/golang-migrate/migrate/releases/download/v4.14.1/migrate.linux-amd64.tar.gz | tar xvz

Затем я перенесу эту вторую команду в этап запуска.

sudo mv migrate.linux-amd64 /usr/bin/migrate

где мы скопируем из builder загруженный двоичный файл migrate в окончательный образ. Нам нужно изменить путь к исходному файлу migrate на /app, потому что это рабочий каталог на этапе сборки, куда мы загружаем и распаковываем файл.

COPY --from=builder /app/migrate.linux-amd64 /usr/bin/migrate

Затем я помещу этот файл в ту же папку WORKDIR на этапе запуска образа, которой также является /app.

COPY --from=builder /app/migrate.linux-amd64 ./migrate

Далее нам также нужно скопировать все файлы SQL миграций из папки db/migration в образ. Поэтому скопируйте db/migration. Я помещу её в папку migration в текущем рабочем каталоге.

COPY db/migration ./migration

Ещё, что нам нужно сделать, так это установить curl на этапе сборки образа, потому что по умолчанию базовый образ Alpine не содержит предустановленного curl. Для этого нам просто нужно добавить туда инструкцию RUN apk add curl. Наконец, мы должны изменить способ запуска приложения. Так, чтобы можно было выполнить миграцию базы данных перед запуском двоичного файла main. Я собираюсь создать новый файл: start.sh в корне нашего проекта. Затем давайте изменим этот файл, чтобы сделать его исполняемым.

chmod +x start.sh

Этот файл будет запускаться с помощью /bin/sh, потому что мы используем образ Alpine, а в нём оболочка bash недоступна. Мы используем инструкцию set -e, чтобы гарантировать, что скрипт немедленно завершится, если команда вернет ненулевой статус. Первый шаг, мы запустим миграцию БД. Итак, мы запускаем двоичный файл /app/migrate, передаём путь к папке, содержащей все файлы SQL миграций, то есть /app/migration. Затем URL-адрес базы данных, который мы возьмем из переменной окружения DB_SOURCE. Итак, здесь я просто использую $DB_SOURCE, чтобы получить его значение. Давайте также воспользуемся опцией -versbose, чтобы подробно выводились все детали при запуске миграции. Наконец, аргумент up используется для применения всех миграций.

set -e

echo "run db migration"
/app/migrate -path /app/migration -database "$DB_SOURCE" -verbose up

После выполнения migrate up мы запустим приложение. Все, что нам нужно сделать на этом шаге, это вызвать exec "$@".

echo "start the app"
exec "$@"

По сути это означает: заменить "$@" на все параметры, переданные скрипту, и запустить на выполнение. В нашем случае мы ожидаем, что передаваться будет /app/main, как определено в этой инструкции CMD.

CMD ["/app/main"]

Чтобы это сработало, мы будем использовать инструкцию ENTRYPOINT и укажем файл /app/start.sh в качестве основной точки входа Docker образа.

ENTRYPOINT ["/app/start.sh"]

Имейте в виду, что когда инструкция CMD используется вместе с ENTRYPOINT, она будет вести себя так, как будто мы передали дополнительные параметры в скрипт точки входа. То есть по сути, это будет похоже на запуск "/app/starts.sh" с "/app/main" в качестве второго аргумента. Но, отделив команду от точки входа, мы получаем большую гибкость и сможем заменить ее другой командой во время выполнения, когда захотим. Подробнее об этом можно прочитать на странице документации Docker, в разделе инструкций CMD.

# Builds stage
FROM golang:1.16-alpine3.13 AS builder
WORKDIR /app
COPY . .
RUN go build -o main main.go
RUN apk add curl
RUN curl -L https://github.com/golang-migrate/migrate/releases/download/v4.14.1/migrate.linux-amd64.tar.gz | tar xvz

# Run stage
FROM alpine3.13
WORKDIR /app
COPY --from=builder /app/main .
COPY --from=builder /app/migrate.linux-amd64 /usr/bin/migrate
COPY app.env .
COPY db/migration ./migration

EXPOSE 8080
CMD ["/app/main"]
ENTRYPOINT ["/app/start.sh"]

Хорошо, теперь когда Docker файл обновлён, давайте попробуем снова запустить docker-compose. Но сначала нам нужно выполнить docker-compose down, чтобы удалить все существующие контейнеры и сети. И мы также должны удалить образ simplebank_api, потому что мы хотим пересобрать образ с учётом новых скриптов для миграции БД.

docker rmi simplebank_api

Итак, после этой команды образ будет удалён. Давайте снова запустим docker-compose up! Для воссоздания образа потребуется некоторое время.

К сожалению, мы получили ошибку: "start.sh: no such file or directory" ("start.sh: нет такого файла или каталога").

Это потому что я забыл скопировать файл start.sh в Docker образ. Итак, давайте сделаем это прямо сейчас!

Здесь на этапе запуска нам просто нужно добавить еще одну инструкцию:

COPY start.sh .

Вот и все! Теперь ошибок быть не должно. Давайте запустим docker-compose down, чтобы удалить все контейнеры. Выполните

docker rmi simplebank_api

чтобы удалить старый образ simplebank_api и наконец, запустите docker-compose up, чтобы всё воссоздать и перезапустить! На этот раз похоже, что скрипт миграции базы данных был запущен, однако он похоже завершился с ошибкой

Ожидаем запуска БД

Сервис завершил свою работу с кодом 1. И в логе мы видим ошибку: "connection refused" ("в соединении отказано"). Приложение не может подключиться к базе данных для выполнения миграции базы данных. Это связано с тем, что серверу Postgres требуется некоторое время, чтобы он был готов принимать запросы, но приложение попыталось запустить миграцию базы данных сразу после его запуска. В тот момент сервер Postgres еще не был готов принять запрос на подключение. Поэтому, чтобы исправить эту ошибку, мы должны указать приложению, что нужно ждать готовности Postgres, прежде чем пытаться запустить скрипт миграции БД и запустить API сервер. Даже несмотря на то, что в файле docker-compose.yaml мы можем использовать инструкции depends_on, чтобы сообщить docker-compose, что сервис api зависит от сервиса postgres, это только гарантирует, что postgres будет запущен до сервиса api. Они не гарантируют, что postgres готов принимать запросы перед запуском сервиса api.

Подробнее об этом поведении можно прочитать на странице документации docker-compose. Просто найдите инструкцию depends_on на странице https://docs.docker.com/compose/compose-file/compose-file-v3/. Там вы увидите, что перечислено несколько особенностей, о которых нужно знать при использовании инструкции depends_on. Если мы хотим дождаться готовности сервиса, мы должны перейти по этой ссылке, чтобы узнать, как управлять порядком запуска. Существует несколько инструментов, написанных для решения этой проблемы, но в нашем случае мы должны воспользоваться sh-совместимым скриптом wait-for, потому что мы используем образ на основе Alpine.

Итак, вот Github страница скрипта. Wait-for предназначен для синхронизации таких сервисов, как Docker контейнеры. И его использовать довольно просто. Нам просто нужно запустить wait-for и передать URL-адрес host:port, готовность которого мы хотим ожидать.

Вот пример,

$ ./wait-for www.eficode.com:80 -- echo "Eficode site is up"
Eficode site is up

По сути в нём ожидается, готовность веб-страницы eficode.com на порту 80 перед выполнением оператора echo. Итак, я собираюсь открыть последнюю версию скрипта и нажать на эту ссылку, чтобы загрузить его (смотри рисунок).

Файл будет загружен в папку Downloads. Поэтому я открою терминал и скопирую этот файл в корень проекта нашего простого банковского приложения. Я переименую его в wait-for.sh, чтобы был более понятен типа файла.

mv ~/Downloads/wait-for ./wait-for.sh

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

chmod +x wait-for.sh

Хорошо, теперь вернемся к нашему Dockerfile. Я собираюсь добавить еще одну инструкцию для копирования файла wait-for.sh в окончательный Docker образ.

COPY wait-for.sh .

Затем в файле docker-compose мы должны переопределить точку входа и команду по умолчанию. Таким образом, он будет ждать готовности сервиса postgres, прежде чем пытаться запустить сервис api. Давайте добавим сюда entrypoint, и ее содержимое должно быть: /app/wait-for.sh. Затем хост и порт, который мы ожидаем, должны быть postgres: 5432, затем два тире, и, наконец, скрипт start.sh, запускаемый после того, как сервер postgres будет готов.

version: "3.9"
services:
  postgres:
    image: postgres:12-alpine
    environment:
      - POSTGRES_USER=root
      - POSTGRES_PASSWORD=secret
      - POSTGRES_DB=simple_bank
  api:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "8080:8080"
    environment:
      - DB_SOURCE=postgresql://root:secret@postgres:5432/simple_bank?sslmode=disable
    depends_on:
      - postgres
    entrypoint: ["/app/wait.sh", "postgres:5432", "--", "/app/start.sh"]

Вы должны понимать, что когда ENTRYPOINT переопределяется таким образом, она также удаляет любую команду по умолчанию в образе, а это означает, что инструкция CMD, которую мы написали в Dockerfile, будет проигнорирована. Таким образом, в этом случае мы также должны явно указать команду, которую мы хотим запустить, в файле docker-compose.yaml. Для этой цели мы используем ключевое слово command. И команда, которую мы хотим запустить, это просто: /app/main.

    entrypoint: [ "/app/wait.sh", "postgres:5432", "--", "/app/start.sh" ]
    command: [ "/app/main" ]

И это всё что нужно сделать! Думаю, теперь у нас есть всё необходимое для успешного запуска приложения. Давайте попробуем еще раз! Сначала выполним docker-compose down, чтобы удалить все существующие контейнеры. Запустите docker rmi, чтобы удалить образ simplebank_api.

docker rmi simplebank_api

И, наконец, выполните

docker-compose up

чтобы пересобрать и перезапустить сервисы. На этот раз вы увидите, что сервисы запускаются по порядку.

Сначала будет запущена сервис postgres, и только когда она будет готова прослушивать соединения, сервис api начнет выполнять миграцию базы данных. Таким образом, миграции были выполнены успешно, сначала миграция init_schema, а затем миграция add_user. Наконец, сервер API был успешно запущен после миграции базы данных (смотри рисунок). Мы видим в логах Gin, что сервер прослушивает и обслуживает HTTP-запросы на порту 8080.

Тестируем Create User API с помощью Postman

Итак, давайте откроем Postman, чтобы протестировать API сервер! Я собираюсь отправить тот же запрос для создания пользователя, что и раньше.

Но на этот раз он выполнен успешно. Создан новый пользователь! Потрясяюще!

На этом я закончу сегодняшнюю лекцию о docker-compose.

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

Большое спасибо за время, потраченное на чтение, желаю вам получать удовольствие от написания кода, и до встречи на следующей лекции!