Привет, Хабр!
Понимаете ли вы пользу документации? Конечно да. Наверняка каждый встречался с ужасно или вовсе не задокументированным проектом. Это всегда больно и с языка так и срываются матерные слова. А бывает, что документация вроде и есть, но разбросана по корпоративному вики, по разным репозиториям, разным директориям. В общем мрак.
В этой статье хотелось бы поговорить про конкретную область для документирования - микросервисы и 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 как инструмент деплоя - на мой личный взгляд это не так красиво, но зато просто.
Надеюсь статья покажется кому-то полезной и побудит попробовать сделать что-то подобное!