Как завести новый сервис¶
Новый сервис = новый git-репозиторий. Этот рецепт — от «есть идея» до
первого зелёного CI и работающего make bootstrap.
Правила структуры, слоёв, Dockerfile, Makefile — в
../conventions/project-layout.md.
Здесь — последовательность шагов.
Содержание¶
- 1. Согласовать границы
- 2. Создать репозиторий
- 3. Клонировать service-template или эталон
- 4. Rename
- 5. Базовые файлы
- 6. Первая миграция
- 7. Wiring в
main.go - 8. Health / readyz
- 9. Metrics
- 10. CI
- 11. Local setup
- 12. Первые тесты
- 13. OpenAPI
- 14. Registry и ownership
- 15. Production-ready check
- Чеклист
- Связанные разделы
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 checkci.
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=user→service=<new>). - Env-префикс если был (например,
NOTIFICATION_*→<NEW>_*). - Порт — свободный из таблицы занятых:
| Сервис | Порт |
|---|---|
| user | 8001 |
| review | 8007 |
| media | 8008 |
| notification | 8013 |
Выбирай следующий неиспользуемый номер. Обнови в .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=kazmaps
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:-kazmaps}
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. Первая миграция¶
migrations/001_init.up.sql:
BEGIN;
SELECT pg_advisory_xact_lock(hashtext('<svc>_migrations'));
CREATE SCHEMA IF NOT EXISTS <svc>;
-- доменные таблицы
CREATE TABLE <svc>.<entity>s (
id BIGSERIAL PRIMARY KEY,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- outbox сразу
CREATE TABLE <svc>.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.sql — DROP SCHEMA <svc> CASCADE; (только если ты
действительно готов терять данные при rollback dev-среды).
Подробности — ../conventions/db-pgx.md и
add-migration.md.
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.md.
9. Metrics¶
Подключи Prometheus client с первого дня, даже если метрик пока 0. Добавление стандартных коллекторов:
prometheus.MustRegister(collectors.NewGoCollector())
prometheus.MustRegister(collectors.NewProcessCollector(collectors.ProcessCollectorOpts{}))
10. CI¶
Скопируй .github/workflows/ci.yml из эталонного сервиса. Минимум job'ов:
lint—golangci-lint run.test—go test -race -count=1 -shuffle=on ./....build—go build ./....docker-build—docker build ..vulncheck—govulncheck ./....
(Когда появится reusable workflow kazmaps-ci — замени на uses: ...@v1.)
11. Local setup¶
После всех шагов должна работать одна команда:
Что она делает:
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 пустым, но
валидным:
Пополняется при добавлении первого endpoint'а.
14. Registry и ownership¶
- Добавь сервис в
../onboarding/04-who-owns-what.md: имя, owner, репозиторий, порт, краткое описание. - Если есть файл
services-catalog.mdв handbook — добавь туда тоже.
15. Production-ready check¶
Перед первым включением прод-трафика пройди
../checklists/production-ready.md:
runbook, alert'ы, graceful shutdown, SBOM, SLO.
Чеклист¶
- Границы сервиса согласованы с lead-инженером.
- Репозиторий создан, branch protection настроен.
- Структура папок совпадает с
project-layout.md. -
main.go— только wiring, есть graceful shutdown. -
migrations/001_init.up.sqlсоздаёт схему + outbox. -
/healthzи/readyzработают. -
/metricsотвечает. -
Dockerfilemultistage + non-root (uid 10001). -
make bootstrapподнимает всё за одну команду. - CI зелёный: lint + test + build + vulncheck.
- Сервис добавлен в who-owns-what.
Связанные разделы¶
../checklists/production-ready.md— финальная проверка перед первым включением prod-трафика.../services-catalog.md— куда добавить запись о новом сервисе.../patterns/outbox.md— outbox обязателен с первого дня, если сервис публикует события.- Conventions, которые должны быть соблюдены с момента создания:
../conventions/project-layout.md,../conventions/go-style.md,../conventions/http-api.md,../conventions/db-pgx.md,../conventions/events.md,../conventions/logging.md,../conventions/observability.md,../conventions/security.md,../conventions/error-handling.md,../conventions/configuration.md,../conventions/shutdown.md,../conventions/testing.md,../conventions/commits-and-prs.md.