Привет, Хабр!

Понимаете ли вы пользу документации? Конечно да. Наверняка каждый встречался с ужасно или вовсе не задокументированным проектом. Это всегда больно и с языка так и срываются матерные слова. А бывает, что документация вроде и есть, но разбросана по корпоративному вики, по разным репозиториям, разным директориям. В общем мрак.

В этой статье хотелось бы поговорить про конкретную область для документирования - микросервисы и REST API. Не буду ходить вокруг да около, конечно же всем известна спецификация OpenAPI, пережившая уже целых три поколения и Swagger, де-факто ставший стандартом индустрии. Речь пойдёт о том, как сделать систему с автоматически актуализируемой Swagger-документацией через использование CI/CD пайплайна и парочки полезных утилит.

Предисловие

Примеры и рассуждения будут приведены для приложения на языке Go, но всё то же самое актуально и для других языков, которые поддерживают подобную генерацию документации.

О чём, собственно, речь

А речь о проекте swaggo/swag, который позволяет описывать документацию в формате Swagger через javadoc-like комментарии к хэндлерам. Самая простая интеграция у проекта с фреймворком gin, но у меня не возникало особых проблем и при использовании echo, и даже "голого" net/http.

Что нужно знать про swag

Итак, немного углубимся в тему. Представим, что у нас есть какое-то несложное (или сложное) веб-приложение. Пусть написали мы его на gin. Значит, в приложении однозначно будет хотя бы один handler, например такой:

func HelloWorld(g *gin.Context)  {
   g.JSON(http.StatusOK, "helloworld")
}

И вот вы захотели для этого жутко сложного метода получить Swagger-документацию. Самый простой путь, что называется "в лоб" - пойти и написать OpenAPI v2 спецификацию вручную, получится что-то вроде такого:

swagger: "2.0"
info:
  title: My API
  description: My API
  license:
    name: MIT
  version: 0.0.1
paths:
  /:
    get:
      summary: Ping
      description: Is used to test connection.
      responses:
        '200':
          description: Success
      

В общем-то незамысловато и так вполне можно поступить, если бы только все приложения были такими маленькими и никогда не расширялись. Каждое изменение в хэндлерах придётся документировать дважды - в комментариях к коду и в этом yaml (или json, если БДСМ сердцу ближе). Но можно этого избежать, примерно вот так:

// @Summary Ping
// @Description Is used to test connection.
// @Accept json
// @Produce json
// @Success 200 {string} helloworld
// @Router / [get]
//
// I'm vanila godoc
func HelloWorld(g *gin.Context)  {
	g.JSON(http.StatusOK, "helloworld")
}

Стало чуть приятнее, не правда ли? Теперь и код сам по себе задокументирован, и никаких отдельных файлов городить не нужно. С помощью таких же комментариев можно описать все остальные хэндлеры, если они есть, а "шапка", то есть блок info, описывается прямо над функцией main - точкой входа в приложение. За подробностями отсылаю к документации, где прекрасно описаны все имеющиеся на данный момент аннотации и дополнительные фишки, такие как описание структуры тела запроса в виде структуры в Go.

После того, как комментарии оставлены, их нужно как-то собрать воедино и превратить это в то, что выше было описано руками. Для этого потребуется cli-утилита swag, самый простой способ установить которую - использовать go install (не забудьте добавить директорию с пакетом в PATH, вероятнее всего это будет ~/go/bin):

go install github.com/swaggo/swag/cmd/swag@latest

Генерация выполняется следующим образом:

swag init -d "directories/with,comments" --parseDependency

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

В результате будут сгенерированы три файла, по умолчанию в директории docs/: docs.go, swagger.yaml и swagger.json. Как не трудно догадаться - это и есть наша документация в разных форматах.

Swagger UI

Наличие docs.go намекает на то, что рядом с приложением можно поднять и Swagger UI. Делается это крайне просто (пример с небольшими изменениями взят из документации):

import (
  "net/http"
  
  docs "module_name/docs"
  
  swaggerfiles "github.com/swaggo/files"
  ginSwagger "github.com/swaggo/gin-swagger"
  "github.com/gin-gonic/gin"
)

func main()  {
   r := gin.Default()
   docs.SwaggerInfo.BasePath = "/api/v1"
   v1 := r.Group("/api/v1")
   {
      eg := v1.Group("/example")
      {
         eg.GET("/helloworld", HelloWorld)
      }
   }
   r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerfiles.Handler))
   r.Run(":8080")

}

Теперь при запуске приложения нетрудно найти Swagger UI, который находится по адресу /swagger/.

Путь к автоматизации

Теперь вернёмся к начальному тезису этой статьи. Одной документации мало, нужно как-то обеспечить её постоянное обновление. Отсутствующие комментарии в коде легко выявляются на этапе ревью кода, а вот просить разработчиков ставить себе swag cli и перегенеривать docs/ каждый раз кажется затратным, да и не барское это дело - автогенерируемый код ревьюить, поэтому пойдём иным путём. Напишем-ка Dockerfile, который бы и документацию генерировал, и приложение билдил, да ещё и с Multi-stage, чтобы по всем канонам. Получим что-то примерно такое:

FROM golang:1.21-alpine AS swag

WORKDIR /app

RUN apk add --no-cache git
RUN go install github.com/swaggo/swag/cmd/swag@latest

COPY . .

RUN go mod tidy && swag init

FROM golang:1.20-alpine AS builder

WORKDIR /app

RUN apk add --no-cache git

COPY go.mod go.sum ./
RUN go mod tidy

COPY --from=swag . .

RUN go build -o /app/main .

FROM scratch

COPY --from=builder /app/main .

RUN apk add --no-cache ca-certificates

CMD ["./main"]

Безусловно это очень плохой Dockerfile, но цель была сделать его как можно понятнее и проще, а не применить все Best Practices.

С Dockerfile покончено. Теперь нужно сбилдить image и запушить в какое-нибудь хранилище. Для следующих шагов очень важно использовать инкрементируемый тег. Например используйте семантическое версионирование.

Деплой приложения

Микросервисы отлично смотрятся в кластере Kubernetes, поэтому задеплоим нашу поделку. Так как для работы всей схемы потребуется ArgoCD и ArgoCD Image Updater, сразу будем писать Helm-чарт. Упрощённо это будет выглядеть вот так:

// values.yaml

replicaCount: 1

image:
  repository: registry.io/app
  tag: 0.0.1
  pullPolicy: Always

service:
  type: ClusterIP
  port: 8080
// templates/deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ .Release.Name }}
  labels:
    app: {{ .Chart.Name }}
  annotations:
    {{- with .Values.annotations }}
    {{ toYaml . | indent 4 }}
    {{- end }}
spec:
  replicas: {{ .Values.replicaCount }}
  selector:
    matchLabels:
      app: {{ .Chart.Name }}
  template:
    metadata:
      labels:
        app: {{ .Chart.Name }}
    spec:
      containers:
        - name: {{ .Chart.Name }}
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
          imagePullPolicy: {{ .Values.image.pullPolicy }}
          ports:
            - containerPort: 8080
// templates/service.yaml

apiVersion: v1
kind: Service
metadata:
  name: {{ .Release.Name }}
  labels:
    app: {{ .Chart.Name }}
spec:
  type: {{ .Values.service.type }}
  ports:
    - port: {{ .Values.service.port }}
      targetPort: 8080
  selector:
    app: {{ .Chart.Name }}

Помимо этого описываем ArgoCD Application:

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: app
  namespace: argocd
  annotations:
    argocd-image-updater.argoproj.io/image-list: app="registry.io/app"
    argocd-image-updater.argoproj.io/app.update-strategy: "semver"
spec:
  project: default
  source:
    repoURL: <git-репозиторий с чартом>
    targetRevision: master
    path: charts/app
    helm:
      valueFiles:
        - values.yaml
  destination:
    server: https://kubernetes.default.svc
    namespace: swag-example
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
    syncOptions:
      - CreateNamespace=true

Post-commit trigger

Этот раздел будет сугубо теоретическим, так как мы используем разные платформы для написания CI и глупо привязываться к чему-то конкретному.
Тем не менее, логика довольно тривиальна - мы уже написали Dockerfile, который умеет билдить приложение и генерировать документацию, уже применили GitOps и даже объяснили ArgoCD, что ему нужно следить за semver-тегами. Теперь дело за малым - научиться обновлять версии образов в автоматическом режиме. Для этого я предлагаю воспользоваться вебхуками, которые умеет отправлять и GitHub, и GitLab, и BitBucket, и даже отечественный GitFlic.

После того, как код прошёл ревью, в комментарии были внесены необходимые правки, а разработчик запушил свои изменения в master-ветку, платформа для CI получает вебхук и запускается пайплайн, который должен сбилдить образ и запушить новую версию в registry. Реализаций этой логики может быть много. Более того, эту логику можно расширить и разделить dev- и prod-контуры. Как бы там ни было, построение системы с постоянно обновляемой документацией завершено.

Заключение

Применение такого подхода позволит качественно документировать свой бэкенд и делать это автоматически, что уменьшает человеческий фактор и не позволит допустить устаревания документации. Да, безусловно предложенная концепция требует сформированной инфраструктуры в виде какой-либо платформы для CI и ArgoCD/FluxCD, но кажется, что такой стек встречается повсеместно. В крайнем случае, никто не мешает использовать тот же GitLab CI как инструмент деплоя - на мой личный взгляд это не так красиво, но зато просто.

Надеюсь статья покажется кому-то полезной и побудит попробовать сделать что-то подобное!

Комментарии (1)