- Woodpecker workflows требуют агент с меткой isolation=dedicated-vm - шаги выполняются в lms-ci:1 с nested dockerd, без хост-Docker сокета - ci/Dockerfile: прогретый immutable-образ (Java, Python, Node, Gradle-кэш) - ci-dind.sh поднимает dockerd внутри privileged-шага; ci-build-image.sh собирает образ - deploy/WOODPECKER.md: отдельная VM, firewall, отсутствие production-секретов, trusted repo, чек-лист безопасности
205 lines
12 KiB
Markdown
205 lines
12 KiB
Markdown
# CI на Gitea + Woodpecker: изолированный контур
|
||
|
||
Репозиторий использует **Woodpecker CI 3.x**. Конфигурация — в `.woodpecker/`:
|
||
|
||
| Workflow | Событие | Что делает |
|
||
|---|---|---|
|
||
| `check` | push, pull_request | тесты, ktlint, detekt, `evidenceCheck` |
|
||
| `smoke` | push, pull_request | после `check`: полный стек, health, вход админа, один курс |
|
||
| `release` | tag, manual | миграции, mutation, load, Chromium/Firefox/WebKit E2E, финальный evidence |
|
||
|
||
## 1. Модель изоляции
|
||
|
||
CI **не должен работать на хосте LMS и не должен иметь доступ к production**.
|
||
Разработчики не считаются доверенными: пайплайн может содержать произвольный
|
||
код, включая `privileged`-контейнеры.
|
||
|
||
Схема:
|
||
|
||
```
|
||
Gitea + Woodpecker server отдельная CI VM
|
||
┌───────────────────────┐ ┌──────────────────────────────┐
|
||
│ Gitea (production) │ webhook │ Woodpecker agent (Docker) │
|
||
│ Woodpecker server │◄────────►│ └─ pipeline step │
|
||
│ │ gRPC │ └─ nested dockerd (dind)│
|
||
└───────────────────────┘ │ ├─ postgres │
|
||
│ ├─ LMS-сервисы │
|
||
│ └─ Playwright │
|
||
└──────────────────────────────┘
|
||
нет prod-сети и секретов
|
||
```
|
||
|
||
Правила:
|
||
|
||
- **Отдельная VM** (или отдельный физический хост) для агента. Не запускайте
|
||
агент на сервере LMS/Gitea.
|
||
- На VM **нет** production-БД, production-томов, deploy-ключей и
|
||
`LMS_INTERNAL_TOKEN`/паролей production. Только одноразовые тестовые значения,
|
||
которые создаёт сам пайплайн.
|
||
- Сетевой firewall: VM может ходить в Gitea/Woodpecker и к registry/пакетным
|
||
зеркалам; **не** может в production-подсеть и к production-БД.
|
||
- VM должна пересоздаваться из чистого снапшота после релиза или по расписанию.
|
||
Всё, что осталось на диске, считается недоверенным.
|
||
- Пайплайн выполняется в образе `lms-ci:1` (на базе `docker:27-dind`) с
|
||
**собственным nested dockerd**.
|
||
Хост-Docker сокет в шаг не монтируется. `privileged` нужен только этому
|
||
вложенному демону; на отдельной VM это допустимо.
|
||
- Агент помечен `isolation=dedicated-vm`; workflows требуют эту метку. Это
|
||
не даёт им случайно попасть на агент, стоящий рядом с production.
|
||
- В Woodpecker не должно быть production-secrets. Если позже понадобится
|
||
деплой — отдельный защищённый агент/пайплайн с ручным подтверждением, не
|
||
этот.
|
||
|
||
> Важно: `privileged`-шаг может скомпрометировать **VM**, но не production.
|
||
> Именно поэтому VM обязательна. Не ослабляйте это правило.
|
||
|
||
## 2. Требования к CI VM
|
||
|
||
- Docker Engine + Compose v2.
|
||
- 4+ CPU, 8+ GB RAM, 40+ GB диска (Gradle, Docker-образы, браузеры).
|
||
- Исходящий доступ к Gitea, registry и пакетным зеркалам.
|
||
- Нет входящих портов, кроме SSH для администрирования.
|
||
- Нет монтирования production-томов и нет production-сетей.
|
||
|
||
## 3. Woodpecker server
|
||
|
||
Server можно держать рядом с Gitea (он не выполняет код пайплайнов). Пример
|
||
`docker-compose.woodpecker.yml` (секреты не коммитить):
|
||
|
||
```yaml
|
||
services:
|
||
woodpecker-server:
|
||
image: woodpeckerci/woodpecker-server:v3
|
||
restart: unless-stopped
|
||
ports:
|
||
- "127.0.0.1:8000:8000" # UI за nginx
|
||
- "127.0.0.1:9000:9000" # gRPC для агента
|
||
environment:
|
||
WOODPECKER_OPEN: "false"
|
||
WOODPECKER_HOST: ${WOODPECKER_HOST:?https://ci.example.ru}
|
||
WOODPECKER_GITEA: "true"
|
||
WOODPECKER_GITEA_URL: ${GITEA_URL:?https://git.example.ru}
|
||
WOODPECKER_GITEA_CLIENT: ${GITEA_CLIENT:?}
|
||
WOODPECKER_GITEA_SECRET: ${GITEA_SECRET:?}
|
||
WOODPECKER_AGENT_SECRET: ${WOODPECKER_AGENT_SECRET:?}
|
||
WOODPECKER_ADMIN: ${WOODPECKER_ADMIN:?admin}
|
||
volumes:
|
||
- woodpecker-data:/var/lib/woodpecker
|
||
|
||
volumes:
|
||
woodpecker-data:
|
||
```
|
||
|
||
1. В Gitea создайте OAuth2-приложение:
|
||
redirect URI `https://ci.example.ru/authorize`.
|
||
2. Заполните `GITEA_CLIENT`, `GITEA_SECRET`, `WOODPECKER_AGENT_SECRET`
|
||
(например, `openssl rand -hex 32`), `WOODPECKER_HOST`, `GITEA_URL`.
|
||
3. Если Gitea и Woodpecker на одном хосте, разрешите webhook на loopback в
|
||
`/etc/gitea/conf/app.ini`:
|
||
```ini
|
||
[webhook]
|
||
ALLOWED_HOST_LIST = external,loopback
|
||
```
|
||
Проверьте, что Gitea отдаёт API-страницы не меньше 50 элементов.
|
||
4. Поднимите server и зайдите в UI через Gitea-аккаунт.
|
||
5. Включите репозиторий LMS в Woodpecker (Settings → Repositories).
|
||
6. В настройках репозитория включите **Trusted** (нужно для `privileged` и
|
||
volumes). Доверять можно только этому приватному репозиторию.
|
||
|
||
## 4. Агент на отдельной VM
|
||
|
||
Агент — в Docker, backend `docker`, с доступом к Docker daemon **этой VM**.
|
||
Шаги пайплайна получают не хостовый сокет, а собственный nested dockerd.
|
||
|
||
`docker-compose.agent.yml` на CI VM:
|
||
|
||
```yaml
|
||
services:
|
||
woodpecker-agent:
|
||
image: woodpeckerci/woodpecker-agent:v3
|
||
restart: unless-stopped
|
||
environment:
|
||
WOODPECKER_SERVER: ${WOODPECKER_SERVER:?https://ci.example.ru:9000}
|
||
WOODPECKER_AGENT_SECRET: ${WOODPECKER_AGENT_SECRET:?}
|
||
WOODPECKER_BACKEND: docker
|
||
WOODPECKER_MAX_WORKFLOWS: "1"
|
||
# Только этот агент должен брать workflows с этой меткой.
|
||
WOODPECKER_AGENT_LABELS: isolation=dedicated-vm
|
||
volumes:
|
||
- /var/run/docker.sock:/var/run/docker.sock
|
||
```
|
||
|
||
```bash
|
||
docker compose -f docker-compose.agent.yml up -d
|
||
docker compose -f docker-compose.agent.yml logs -f
|
||
```
|
||
|
||
Дополнительно в UI откройте **Settings → Agents** и задайте агенту фильтр
|
||
`isolation=dedicated-vm` — это серверная проверка, которую агент не может
|
||
подделать.
|
||
|
||
**Общего кэша между пайплайнами нет.** Каждый шаг получает чистый контейнер,
|
||
а вложенный Docker storage живёт только внутри него. Это важно: разработчик не
|
||
может оставить артефакт, который повлияет на следующий pipeline или релиз.
|
||
|
||
Пайплайны используют предварительно прогретый образ `lms-ci:1`. Соберите его
|
||
на VM из **проверенного администратором** коммита до первого запуска:
|
||
|
||
```bash
|
||
git checkout <проверенный-коммит>
|
||
./scripts/ci-build-image.sh lms-ci:1
|
||
```
|
||
|
||
Сборка образа выполняет Gradle-код из репозитория. Не собирайте образ из
|
||
непроверенной ветки/PR: это запуск недоверенного кода на CI VM.
|
||
|
||
Образ содержит Docker-in-Docker, Java 21, Python, bash, Node и прогретый
|
||
Gradle-кэш; исходники всё равно берутся из checkout. Пересобирайте образ при
|
||
изменении зависимостей или базовых версий. Не монтируйте в шаги общие
|
||
writable-кэши: это канал влияния одного пайплайна на другой.
|
||
|
||
## 5. Как устроены workflows
|
||
|
||
- `check`, `smoke` и `release` требуют `labels.isolation=dedicated-vm`.
|
||
- `smoke` ждёт успешного `check` (`depends_on`).
|
||
- Все workflows имеют `concurrency: {limit: 1, group: lms-ci}`, потому что
|
||
smoke/release поднимают стек на портах 8080–8090.
|
||
- `check` и `smoke`/`release` используют `lms-ci:1` + `./scripts/ci-dind.sh`:
|
||
скрипт поднимает **вложенный** dockerd. Образ собирается из `ci/Dockerfile`.
|
||
- `release` перед миграционным тестом делает `git fetch` master: CI-клон может
|
||
быть shallow.
|
||
- `smoke`/`release` не получают хост-Docker сокет; `docker compose` работает
|
||
с вложенным демоном.
|
||
|
||
## 6. Первый прогон
|
||
|
||
0. На CI VM соберите образ: `./scripts/ci-build-image.sh lms-ci:1`.
|
||
1. Push в ветку или MR → `check`, затем `smoke`.
|
||
2. Убедитесь, что оба workflow зелёные и выполнялись на агенте с меткой
|
||
`isolation=dedicated-vm`.
|
||
3. Проверьте ручной запуск: UI → Run pipeline → event `manual` → workflow
|
||
`release`.
|
||
4. Для релиза: `git tag vX.Y.Z && git push origin vX.Y.Z` → `release`.
|
||
5. После релиза/по расписанию пересоздайте VM из чистого снапшота.
|
||
|
||
Если pipeline не появляется:
|
||
|
||
- репозиторий включён в Woodpecker;
|
||
- у агента и server совпадает `WOODPECKER_AGENT_SECRET`;
|
||
- агент видит Docker: `docker compose version` на VM;
|
||
- метка агента `isolation=dedicated-vm` и серверный фильтр совпадают;
|
||
- путь конфигурации проекта — `.woodpecker/` (по умолчанию);
|
||
- образ `lms-ci:1` собран на VM (иначе шаг не стартует);
|
||
- репозиторий помечен trusted, иначе `privileged` будет отклонён.
|
||
|
||
## 7. Чек-лист безопасности
|
||
|
||
- [ ] Агент не на production-хосте.
|
||
- [ ] На VM нет production-секретов, БД и томов.
|
||
- [ ] Firewall запрещает VM → production.
|
||
- [ ] В Woodpecker нет production-secrets.
|
||
- [ ] Включён trusted только для LMS, не для чужих репозиториев.
|
||
- [ ] `WOODPECKER_AGENT_SECRET` уникален и хранится вне репозитория.
|
||
- [ ] VM пересоздаётся из снапшота; кэш не считается доверенным.
|
||
- [ ] `privileged` разрешён только на этой VM, не на общем runner.
|