Skip to Content
How-toЗавести новый сервис

Как завести новый сервис

Новый сервис = новый git-репозиторий. Этот рецепт — от «есть идея» до первого зелёного CI и работающего make bootstrap.

Правила структуры, слоёв, Dockerfile, Makefile — в ../conventions/project-layout. Здесь — последовательность шагов.

Содержание

1. Согласовать границы

До кода обсуди с lead-инженером:

  • Bounded context. Какая доменная область у сервиса? Сервис = один bounded context по DDD. Два контекста в одном сервисе — запрет.
  • Какие сущности он владеет (таблицы, инварианты).
  • Какие события публикует (<entity>.created, <entity>.updated).
  • Какие события потребляет (из каких других сервисов).
  • Downstream HTTP. Какие /internal/* endpoint’ы ему придётся дёргать у соседей.

Записывай результат в 1-страничный spec в PR-описании первого коммита.

2. Создать репозиторий

  • Имя репозитория: <service>-service (например, order-service, search-service).
  • Тип: private по умолчанию.
  • Настрой branch protection на main: minimum 1 approve, require status check ci.

3. Клонировать service-template или эталон

Когда появится kazmaps-service-template — клонируй его. Пока шаблона нет — возьми за основу эталонный сервис:

  • самый чистый — репозиторий сервиса user, файлы cmd/server/main.go, internal/handler/router.go, Makefile, Dockerfile.
  • если сервис событийный (много Kafka-handler’ов) — репозиторий сервиса notification.

Скопируй структуру папок и набор файлов в свой новый репозиторий. Бизнес-код эталона удали, оставь только каркас:

<new-service>/ ├── cmd/server/main.go ├── internal/{config,handler,middleware,service,repository/postgres,event,domain,db}/ ├── pkg/db/ ├── migrations/ ├── docker-compose.yml ├── Dockerfile ├── Makefile ├── go.mod ├── .env.example └── README.md

4. Rename

Переименуй всё, что завязано на имя эталона:

  • go.mod: module github.com/example/<service>-service.

  • Имя сервиса в логах (service=userservice=<new>).

  • Env-префикс если был (например, NOTIFICATION_*<NEW>_*).

  • Порт — свободный из таблицы занятых:

    СервисПорт
    user8001
    review8007
    media8008
    notification8013

    Выбирай следующий неиспользуемый номер. Обнови в .env.example, docker-compose.yml, Dockerfile (EXPOSE), internal/config/.

5. Базовые файлы

README.md

Одна страница: зачем сервис, как запустить локально, где документация на контракты (OpenAPI).

.env.example

Все переменные из internal/config/ с dev-значениями:

SERVICE_PORT=8020 SERVICE_LOG=info DB_HOST=localhost DB_PORT=5432 DB_USER=<svc> DB_PASSWORD=<svc>_dev_password DB_NAME=<svc>_db REDIS_ADDR=localhost:6379 KAFKA_BROKERS=localhost:9092 KAFKA_CLIENT_ID=<svc> INTERNAL_API_TOKEN=change_me_internal_token GATEWAY_HMAC_KEY=change_me_hmac_key

Makefile

Минимум целей:

.PHONY: help bootstrap deps build run test lint up down logs psql migrate-up migrate-down help: @grep -E '^[a-zA-Z_-]+:.*## ' Makefile | awk 'BEGIN{FS=":.*## "};{printf "%-15s %s\n", $$1, $$2}' bootstrap: ## .env + deps + docker up + migrations cp -n .env.example .env || true $(MAKE) deps $(MAKE) up deps: ## download go modules go mod download && go mod tidy build: ## build binary to bin/ CGO_ENABLED=0 go build -ldflags="-s -w" -o bin/server ./cmd/server run: ## run binary (expects infra up) ./bin/server test: ## run tests with race detector go test -race -count=1 ./... lint: ## run golangci-lint golangci-lint run up: ## docker compose up -d docker compose up -d down: ## docker compose down (keeps volumes) docker compose down clean: ## docker compose down -v (drops volumes) docker compose down -v logs: ## follow service logs docker compose logs -f <service> psql: ## psql in postgres container docker compose exec postgres psql -U $${DB_USER:-<service>} -d $${DB_NAME:-<service>_db} migrate-up: ## apply all migrations migrate -path ./migrations -database "$${DB_DSN}" up migrate-down: ## rollback one migration migrate -path ./migrations -database "$${DB_DSN}" down 1

Dockerfile

FROM golang:1.25-alpine AS builder WORKDIR /app COPY go.mod go.sum ./ RUN go mod download COPY . . RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /out/svc ./cmd/server FROM alpine:3.20 RUN apk add --no-cache ca-certificates tzdata \ && adduser -D -u 10001 app COPY --from=builder /out/svc /bin/svc USER app EXPOSE 8020 ENTRYPOINT ["/bin/svc"]
  • CGO_ENABLED=0 — статическая сборка, работает в alpine.
  • USER app (uid 10001) — non-root runtime.
  • -ldflags="-s -w" — убирает отладочные символы, бинарь меньше.

docker-compose.yml

postgres + redis + kafka + сам сервис. Образец — из docker-compose.yml эталонного сервиса. Не забудь healthcheck’и и depends_on: condition: service_healthy, чтобы сервис не стартовал раньше БД.

6. Первая миграция

У каждого сервиса — своя отдельная PostgreSQL-БД; schemas-per-service внутри общего кластера не используем. Provisioning самой БД (CREATE DATABASE, DB-user с ограниченными правами, grant’ы) — задача инфраструктуры (terraform/helm в infra-репо), а не application-миграций. Сервис получает готовый DSN через env (<SVC>_DB_*) и подключается к уже существующей БД.

migrations/001_init.up.sql создаёт таблицы в схеме public (Postgres default), без префикса с именем сервиса:

BEGIN; SELECT pg_advisory_xact_lock(hashtext('<svc>_migrations')); -- доменные таблицы CREATE TABLE <entity>s ( id BIGSERIAL PRIMARY KEY, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- outbox сразу CREATE TABLE outbox ( "offset" BIGSERIAL PRIMARY KEY, uuid UUID NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), payload BYTEA, metadata JSONB, transaction_id XID8 NOT NULL DEFAULT pg_current_xact_id() ); -- Схема конкретных колонок outbox определяется watermill-sql -- DefaultPostgreSQLSchema; сверься с версией модуля при создании. COMMIT;

001_init.down.sqlDROP TABLE <entity>s, outbox; (только если ты действительно готов терять данные при rollback dev-среды).

Подробности — ../conventions/db-pgx и add-migration.

7. Wiring в main.go

func run() error { cfg, err := config.Load() if err != nil { return err } log := newLogger(cfg.Service.Log) log.Info("starting service", "service", cfg.Service.Name, "port", cfg.Service.Port) ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) defer cancel() database, err := pkgdb.New(ctx, pkgdb.Config{DSN: cfg.DB.DSN(), PoolMax: 20, PoolMin: 2}) if err != nil { return fmt.Errorf("db: %w", err) } defer database.Close() if err := migrations.Run(ctx, database, log); err != nil { return fmt.Errorf("migrate: %w", err) } rdb := redis.NewClient(&redis.Options{Addr: cfg.Redis.Addr}) if err := rdb.Ping(ctx).Err(); err != nil { return fmt.Errorf("redis: %w", err) } defer rdb.Close() kafkaPub, kafkaSub, err := event.NewKafka(cfg.Kafka) if err != nil { return fmt.Errorf("kafka: %w", err) } defer kafkaPub.Close() defer kafkaSub.Close() // собрать repositories / services / handlers ... router := handler.NewRouter(handler.Deps{ /* ... */ }) srv := &http.Server{ Addr: cfg.Service.Addr(), Handler: router, ReadHeaderTimeout: 10 * time.Second, } errCh := make(chan error, 1) go func() { errCh <- srv.ListenAndServe() }() go func() { errCh <- forwarder.Run(ctx) }() select { case <-ctx.Done(): shutdownCtx, c := context.WithTimeout(context.Background(), 30*time.Second) defer c() return srv.Shutdown(shutdownCtx) case err := <-errCh: return err } }

Правила:

  • main.goтолько wiring. Никакой бизнес-логики.
  • Graceful shutdown на SIGINT/SIGTERM: ловим сигнал, делаем srv.Shutdown(ctx) с таймаутом, выходим.
  • Fail-fast на старте: если pool.Ping / redis.Ping упали — сервис не стартует.

8. Health / readyz

Hard-обязательно:

r.Get("/healthz", d.Health.Live) // liveness: просто 200 r.Get("/readyz", d.Health.Ready) // readiness: pool.Ping + redis.Ping + kafka.Ping

См. ../conventions/http-api.

9. Metrics

r.Handle("/metrics", promhttp.Handler())

Подключи Prometheus client с первого дня, даже если метрик пока 0. Добавление стандартных коллекторов:

prometheus.MustRegister(collectors.NewGoCollector()) prometheus.MustRegister(collectors.NewProcessCollector(collectors.ProcessCollectorOpts{}))

10. CI

Скопируй .github/workflows/ci.yml из эталонного сервиса. Минимум job’ов:

  • lintgolangci-lint run.
  • testgo test -race -count=1 -shuffle=on ./....
  • buildgo build ./....
  • docker-builddocker build ..
  • vulncheckgovulncheck ./....

(Когда появится reusable workflow kazmaps-ci — замени на uses: ...@v1.)

11. Local setup

После всех шагов должна работать одна команда:

make bootstrap

Что она делает:

  1. Копирует .env.example в .env, если .env нет.
  2. go mod download && go mod tidy.
  3. docker compose up -d — поднимает postgres/redis/kafka/сервис.
  4. Ждёт healthcheck’и.
  5. Применяет миграции (если migrate не встроен в сервис).

Целевое состояние: коллега клонирует репо, делает make bootstrap, через 30 секунд сервис отвечает 200 на /healthz.

12. Первые тесты

Минимум для зелёного CI:

// internal/handler/health_test.go func TestHealthLive(t *testing.T) { h := handler.NewHealthHandler(nil, nil) req := httptest.NewRequest(http.MethodGet, "/healthz", nil) rec := httptest.NewRecorder() h.Live(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status: %d", rec.Code) } }

Дальше добавляй тесты на service/handler по мере появления бизнес-кода. Race detector — сразу в make test.

13. OpenAPI

Если сервис держит публичный API — создай api/openapi.yaml пустым, но валидным:

openapi: 3.1.0 info: title: <service> API version: 0.1.0 paths: {} components: {}

Пополняется при добавлении первого endpoint’а.

14. Registry и ownership

  • Добавь сервис в ../onboarding/04-who-owns-what: имя, owner, репозиторий, порт, краткое описание.
  • Если есть файл services-catalog.md в handbook — добавь туда тоже.

15. Production-ready check

Перед первым включением прод-трафика пройди ../checklists/production-ready: runbook, alert’ы, graceful shutdown, SBOM, SLO.

Чеклист

  • Границы сервиса согласованы с lead-инженером.
  • Репозиторий создан, branch protection настроен.
  • Структура папок совпадает с project-layout.
  • main.go — только wiring, есть graceful shutdown.
  • migrations/001_init.up.sql создаёт доменные таблицы + outbox в БД сервиса (БД заведена инфрой).
  • /healthz и /readyz работают.
  • /metrics отвечает.
  • Dockerfile multistage + non-root (uid 10001).
  • make bootstrap поднимает всё за одну команду.
  • CI зелёный: lint + test + build + vulncheck.
  • Сервис добавлен в who-owns-what.

Связанные разделы

Last updated on