Перейти к содержанию

Как добавить новый сервис?

Сервисы условно можно разделить на внутренние и внешние.

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

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

Соответственно, наиболее актуальный вопрос — как добавить новый внешний сервис?

Как добавить новый внешний сервис?

Подсказка

Для создания нового сервиса можно воспользоваться существующими файлами:

Конфигурация сервиса задаётся в файле docker-compose.swarm.yml.

Этот файл задаёт конфигурацию в формате docker-compose.

Пример docker-compose.swarm.yml для сервиса domain
version: "3.9"

services:
  domain-server:
    image: ${REGISTRY_HOST}/domain-server:latest
    volumes:
    - ./configs:/data/conf
    - ./storage:/storage
    logging:
      driver: "json-file"
      options:
        max-size: 10m
        max-file: "3"
        tag: "{{.ImageName}}|{{.Name}}|{{.ID}}"
    deploy:
      labels:
        traefik.enable: "true"
        traefik.backend: domain
        traefik.http.routers.domain.entrypoints: https
        traefik.http.routers.domain.tls: "true"
        traefik.http.routers.domain.tls.certresolver: letsencrypt
        traefik.http.routers.domain.rule: Host(`domain.${SUBDOMAIN}.${DOMAIN}`)
        traefik.http.services.domain.loadbalancer.server.port: 8000
      placement:
        constraints:
          - "node.labels.cluster==swarm"
      mode: replicated
      replicas: 1
      update_config:
        parallelism: 1
        order: start-first
        failure_action: rollback
        delay: 10s
      rollback_config:
        parallelism: 0
        order: stop-first
      restart_policy:
        condition: any
        delay: 5s
        max_attempts: 3
        window: 120s

  domain-integrator:
    image: ${REGISTRY_HOST}/domain-integrator:latest
    volumes:
      - ./configs:/data/conf
      - ./storage:/storage
    logging:
      driver: "json-file"
      options:
        max-size: 10m
        max-file: "3"
        tag: "{{.ImageName}}|{{.Name}}|{{.ID}}"
    deploy:
      labels:
        traefik.enable: "false"
      placement:
        constraints:
          - "node.labels.cluster==swarm"
      mode: replicated
      replicas: 1
      update_config:
        parallelism: 1
        order: start-first
        failure_action: rollback
        delay: 10s
      rollback_config:
        parallelism: 0
        order: stop-first
      restart_policy:
        condition: any
        delay: 5s
        max_attempts: 3
        window: 120s

  domain-notifier:
    image: ${REGISTRY_HOST}/domain-notifier:latest
    volumes:
      - ./configs:/data/conf
      - ./storage:/storage
    logging:
      driver: "json-file"
      options:
        max-size: 10m
        max-file: "3"
        tag: "{{.ImageName}}|{{.Name}}|{{.ID}}"
    deploy:
      labels:
        traefik.enable: "false"
      placement:
        constraints:
          - "node.labels.cluster==swarm"
      mode: replicated
      replicas: 1
      update_config:
        parallelism: 1
        order: start-first
        failure_action: rollback
        delay: 10s
      rollback_config:
        parallelism: 0
        order: stop-first
      restart_policy:
        condition: any
        delay: 5s
        max_attempts: 3
        window: 120s

  domain-docs:
    image: ${REGISTRY_HOST}/domain-docs:latest
    logging:
      driver: "json-file"
      options:
        max-size: 10m
        max-file: "3"
        tag: "{{.ImageName}}|{{.Name}}|{{.ID}}"
    deploy:
      labels:
        traefik.enable: "true"
        traefik.backend: domain-docs
        traefik.http.routers.domain-docs.entrypoints: https
        traefik.http.routers.domain-docs.tls: "true"
        traefik.http.routers.domain-docs.tls.certresolver: letsencrypt
        traefik.http.routers.domain-docs.rule: Host(`docs.domain.${SUBDOMAIN}.${DOMAIN}`)
        traefik.http.services.domain-docs.loadbalancer.server.port: 80
      placement:
        constraints:
          - "node.labels.cluster==swarm"
      mode: replicated
      replicas: 1
      update_config:
        parallelism: 1
        order: start-first
        failure_action: rollback
        delay: 10s
      rollback_config:
        parallelism: 0
        order: stop-first
      restart_policy:
        condition: any
        delay: 5s
        max_attempts: 3
        window: 120s
    healthcheck:
      test: wget --no-verbose --tries=1 --spider http://localhost/ || exit 1
      interval: 30s
      timeout: 3s
      retries: 12

Помимо основных ключей конфигурации, используются ключи logging, deploy и healthcheck.

Секция logging

Параметр logging отвечает за формат формирования логов (позже по ним можно будет искать в Loki).

logging yaml example
    logging:
      driver: "json-file"
      options:
        max-size: 10m
        max-file: "3"
        tag: "{{.ImageName}}|{{.Name}}|{{.ID}}"

Формат тега задан в виде {{.ImageName}}|{{.Name}}|{{.ID}} для корректного парсинга в promtail.

Пример конфигурации promtail.yml, с секцией отвечающей за парсинг логов
# ...
scrape_configs:
  # ...
  - job_name: containers
    # ...
    pipeline_stages:
      # ...
      - regex:
          # expression set by tag: "{{.ImageName}}|{{.Name}}|{{.ID}}"
          expression: ^(?P<image>([^|]+))\|(?P<service>([^|]+))\|(?P<container_id>([^|]+))$
          source: "tag"
      - labels:
          docker_image: image
          docker_service: service
          docker_container_id: container_id
          docker_timestamp: timestamp

Документация секции loggingDocker Docs | JSON File logging driver.

Секция deploy

Параметр deploy отвечает за развёртывание сервиса в кластере Docker Swarm:

Пример описания секции deploy
deploy:
  labels:
    traefik.enable: "true"
    traefik.backend: domain-docs
    traefik.http.routers.domain-docs.entrypoints: https
    traefik.http.routers.domain-docs.tls: "true"
    traefik.http.routers.domain-docs.tls.certresolver: letsencrypt
    traefik.http.routers.domain-docs.rule: Host(`docs.domain.${SUBDOMAIN}.${DOMAIN}`)
    traefik.http.services.domain-docs.loadbalancer.server.port: 80
  placement:
    constraints:
      - "node.labels.cluster==swarm"
  mode: replicated
  replicas: 1
  update_config:
    parallelism: 1
    order: start-first
    failure_action: rollback
    delay: 10s
  rollback_config:
    parallelism: 0
    order: stop-first
  restart_policy:
    condition: any
    delay: 5s
    max_attempts: 3
    window: 120s

Для вынесения сервиса на домен или поддомен используется секция deploy.labels.

Шаблон секции labels при добавления нового сервиса
labels:
  traefik.enable: "true"
  traefik.backend: {{service_name}}
  traefik.http.routers.{{service_name}}.entrypoints: https
  traefik.http.routers.{{service_name}}.tls: "true"
  traefik.http.routers.{{service_name}}.tls.certresolver: letsencrypt
  traefik.http.routers.{{service_name}}.rule: Host(`{{service_name}}.${SUBDOMAIN}.${DOMAIN}`)
  traefik.http.services.{{service_name}}.loadbalancer.server.port: {{service_port}}

Для примера выше вместо {{service_name}} можно указать имя сервиса и вместо {{service_port}} номер порта, который должен быть выведен наружу.

В таком случае, на https://{{service_name}}.${SUBDOMAIN}.${DOMAIN} (на стандартный для TLS порт 443) будет выведен указанный порт сервиса.

Описание секции deployDocker Docs | Compose Deploy Specification.

Секция healthcheck

Используется для проверки работоспособности сервиса.

Уместно при новом развёртывании сервиса — трафик не будет переключаться на новый сервис, пока у него не пройдёт healthcheck.

Таким образом, наличие секции healthcheck позволяет деплоить сервисы бесшовно.

Пример секции healthcheck для абстрактного сервиса
healthcheck:
  test: wget --no-verbose --tries=1 --spider http://localhost/ || exit 1
  interval: 30s
  timeout: 3s
  retries: 12
Пример секции healthcheck для backend-сервиса на базе go-kratos
healthcheck:
  test: wget --no-verbose --tries=1 --spider http://localhost:8000/healthcheck || exit 1
  interval: 30s
  timeout: 3s
  retries: 12

Примечание

wget вместо curl используется потому, что wget встречается в образах чаще. Например, он включён в состав образа alpine.

Описание секции — Docker Docs | healthcheck.

Dockerfile-*

Сам по себе сервис может состоять из нескольких приложений-сервисов. Такие "суб-сервисы" задаются с помощью файлов Dockerfile-<service>.

??? Пример файла Dockerfile-server

```dockerfile
FROM golang:1.19 AS builder

COPY . /src
WORKDIR /src

RUN make build

FROM alpine:latest

COPY --from=builder /src/bin /app

WORKDIR /app

EXPOSE 8000
EXPOSE 9000
EXPOSE 13000
VOLUME /data/conf

CMD ["./server", "-conf", "/data/conf"]
```

В Makefile.extended.mk можно встретить секции, работающие с такими файлами.

Пример команды make push
.PHONY: push
# Build and push image to registry
push:
    @set -e; for service in $$(ls Dockerfile-* | cut -c 12-); \
        do docker build -t ${REGISTRY_HOST}/${SERVICE_NAME}-$${service}:latest \
            -f ${CURRENT_DIRECTORY}/Dockerfile-$${service} ${CURRENT_DIRECTORY}/. \
            && docker push ${REGISTRY_HOST}/${SERVICE_NAME}-$${service}:latest ; \
    done

Наличие одновременно и docker-compose.swarm.yml, и Dockerfile-* может запутать.

В чём разница?

Файл docker-compose.swarm.yml:

  • Нацелен на запуск сервиса и его "суб-сервисов";
  • Описывает конфигурацию для запуска сервиса (на целевом сервере от всего репозитория иногда нужен только этот файл, исходный код не участвует в развёртывании);
  • Опирается как на внешние образы, так и на внутренние образы, формируемые с помощью файлов Dockerfile-* и загружаемые в registry созданного кластера.

Файл Dockerfile-*:

  • Нацелен на создание Docker-образов для "суб-сервисов";
  • Описывает команды для сборки образа нужного "суб-сервиса";
  • Собранный с его помощью образ можно загрузить в registyr созданного кластера.

Как добавить новый внутренний сервис?

В большинстве случаев, нужно повторить логику существующих внутренних сервисов:

  • Создать каталог, который называется также, как и сервис
  • В каталоге разместить docker-compose.swarm.yaml
  • Настроить по аналогии с другими файлами docker-compose.swarm.yaml
  • Учесть сервис в Makefile — если он должен запускаться вместе с кластером, то добавить название сервиса в команды make run-only и make down