k3s-лаборатория на одном VPS: GitOps, Gitea и Grafana
КейсыService Lab.
k3s — это «маленький Kubernetes»: тот же API и те же манифесты, но ставится одной командой и живёт на одной виртуалке. Мы собрали на нём полноценную лабораторию: git-хостинг с CI, мониторинг, базы данных, объектное хранилище и сам сайт service-lab.online. Всё, что принято называть «инфраструктурой продакшена», умещается в одном репозитории.
Зачем это нужно? Лаборатория — полигон: любое изменение инфраструктуры сначала обкатывается здесь, а не в боевом проекте клиента. При этом это не «игрушка» — кластер обслуживает реальные сервисы с TLS-сертификатами, бэкапами и графиками дашбордов.
Что внутри
Стек лаборатории:
- k3s server на одном VPS, всё состояние — на локальном SSD;
- Traefik как ingress-контроллер с редиректом на HTTPS;
- cert-manager и Let's Encrypt, wildcard-сертификат
*.service-lab.online; - sealed-secrets — шифрованные секреты прямо в git;
- Postgres и Valkey как общие базы;
- MinIO — S3-совместимое хранилище;
- kube-prometheus-stack (Prometheus, Grafana, Alertmanager), Loki с Promtail для логов, Tempo и OpenTelemetry для трейсов;
- Gitea с Actions-раннером — git-хостинг и CI;
- Outline как база знаний.
По сути это микрокосм нормального продакшена: каждый компонент — тот же, что стоял бы в кластере «на десяток команд», только в одном экземпляре.
Один репозиторий вместо десяти
Весь кластер описан в монорепозитории с двумя корнями: gitops/ — инфраструктура и общие сервисы, apps/ — собственные приложения:
gitops/
├── infrastructure/ # traefik, storage, cert-manager, kubeseal
├── shared-services/ # postgres, valkey, minio, logging-stack
└── applications/ # gitea (+ act-runner), outline-wiki
apps/
├── service-lab.online/ # исходники + Dockerfile + k8s/
└── demo-axum/ # Rust-демо с CI-деплоем
Исходники и манифесты живут рядом: поменял код — CI собрал образ и обновил деплой. Поменял конфиг инфраструктуры — переприменил папку. Это тот же принцип «просто и в одном месте», за который мы любим чистый монолит вместо микросервисов, только на уровне инфраструктуры.
Загрузка с нуля
Кластер поднимается по документированному порядку шагов, где у каждого есть проверка результата:
- Traefik — ingress-контроллер, IngressClass по умолчанию;
- PVC под базы и хранилища — все в статусе Bound;
- cert-manager — сертификаты Let's Encrypt, issuer Ready;
- sealed-secrets — контроллер и публичный ключ, закоммиченный в репозиторий;
- Postgres и Valkey — общие сервисы данных;
- MinIO — S3 API отвечает 200;
- стек наблюдаемости — Prometheus, Grafana, Loki, Promtail, OTel-коллектор;
- Gitea и Actions-раннер — раннер виден в админке.
Вся процедура записана в README репозитория: от чистой виртуалки до работающего CI — меньше вечера.
Секреты, которые можно коммитить
Все секреты в репозитории — SealedSecrets: они зашифрованы публичным ключом контроллера, и расшифровать их может только кластер:
# пишем обычный Secret во временный файл (в git не попадёт)
cat > /tmp/plain.yaml <<EOF
apiVersion: v1
kind: Secret
stringData:
TOKEN: example
EOF
# запечатываем в репозиторий, рядом с манифестами
kubeseal --cert gitops/infrastructure/kubeseal/pub-cert.pem \
--format yaml < /tmp/plain.yaml \
> gitops/shared-services/my-secret.yaml
rm /tmp/plain.yaml
В git уходит только шифрованый YAML. Отдельно храним резервную копию ключа контроллера — без неё переезд на новый кластер означал бы перепечатывание всех секретов.
От push до продакшена — 2,5 минуты
CI — Gitea Actions, полностью внутри кластера. Пуш в develop или main запускает pipeline: раннер клонирует репозиторий и создаёт обычный Kubernetes Job с kaniko — образом собирает без Docker-демона:
apiVersion: batch/v1
kind: Job
metadata:
name: kaniko-service-lab-a1b2c3d
spec:
template:
spec:
restartPolicy: Never
initContainers:
- name: git-clone
image: alpine/git:v2.47.2
args: ["git", "clone", "--depth", "1", "...", "/workspace"]
containers:
- name: kaniko
image: gcr.io/kaniko-project/executor:v1.23.2
resources:
requests: {cpu: "2", memory: 2Gi}
limits: {cpu: "8", memory: 12Gi}
args:
- --dockerfile=/workspace/apps/service-lab.online/Dockerfile
- --destination=gitea.service-lab.online/gitops/service-lab:a1b2c3d
Дальше pipeline применяет манифесты, пинит образ к SHA коммита и проверяет раскатку. От git push до живого сайта проходит около двух с половиной минут — Rust-сборка в кластере получает 8 ядер и 12 ГБ памяти, чтобы не растягивать ожидание.
Никакого внешнего CI: раннер, registry и цели деплоя — один и тот же кластер. Контур замкнут, и за его безопасность отвечает тот самый SealedSecret с токеном раннера.
Наблюдаемость и бэкапы
Логи любого пода — в Grafana через Loki, метрики — в Prometheus, трейсы — в Tempo через OpenTelemetry-коллектор. Для лаборатории это избыточно ровно до того момента, когда что-то ломается ночью — и по графику за десять секунд видно, что именно.
Бэкапы — CronJob'ы по расписанию: дампы Postgres, снапшоты MinIO и копии репозиториев Gitea. Восстановление проверяем так же цинично, как и бэкап — периодическим тестовым подъёмом.
Повседневность
Ежедневная работа с кластером умещается в две команды:
# обновить чарт: поднять версию в kustomization.yaml, затем
kustomize build gitops/shared-services/databases --enable-helm | kubectl apply -f -
# что-то разъехалось с git? переприменить папку — всё идемпотентно
Желаемое состояние всегда в git, кластер приводится к нему повторным применением. drift-страх перед «ручными правками на сервере» исчезает: править руками бессмысленно — всё равно перезапишется.
Итог
Один VPS, один репозиторий, один push до продакшена.
k3s-лаборатория окупается быстро: эксперименты с инфраструктурой перестают быть риском, а новые сервисы находят себе место в готовом окружении — с TLS, секретами, мониторингом и CI «из коробки».
О том, почему собственные сервисы мы пишем на Rust — в статье Rust и Zig в привычных приложениях.
Обсудим ваш проект?
Расскажите о вашей задаче — вернёмся с оценкой сроков и стоимости в течение двух рабочих дней.