Compare commits
20
Commits
f1a3ec00dd
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
242a066f42 | ||
|
|
4ad904efd0 | ||
|
|
690b1fdb25 | ||
|
|
515e221c52 | ||
|
|
4e67059f7a | ||
|
|
2a1da643c1 | ||
|
|
09f2c839f7 | ||
|
|
c5e9bfe6b3 | ||
|
|
1edda33f87 | ||
|
|
8584fc225b | ||
|
|
1ae670410b | ||
|
|
6983c25d10 | ||
|
|
ad60d0c738 | ||
|
|
afcce5dc4a | ||
|
|
efa4dd9a3e | ||
|
|
70d6da761f | ||
|
|
6f61c0ee90 | ||
|
|
9d8696f748 | ||
|
|
a4fa66c466 | ||
|
|
d19eb2de9d |
@@ -0,0 +1,2 @@
|
|||||||
|
mkdocs-material==9.6.14
|
||||||
|
mkdocs-same-dir==0.1.3
|
||||||
Executable
+31
@@ -0,0 +1,31 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
readonly venv_dir="/var/lib/gitea-runner/venvs/site"
|
||||||
|
|
||||||
|
if [[ $# -ne 1 ]]; then
|
||||||
|
echo "Usage: $0 SOURCE_DIR" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
source_dir=$(realpath "$1")
|
||||||
|
requirements_file="${source_dir}/.gitea/requirements-site.txt"
|
||||||
|
|
||||||
|
if [[ ! -f $requirements_file ]]; then
|
||||||
|
echo "Requirements file not found: ${requirements_file}" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$(dirname "$venv_dir")"
|
||||||
|
if [[ ! -x "${venv_dir}/bin/python" ]]; then
|
||||||
|
python3 -m venv "$venv_dir"
|
||||||
|
fi
|
||||||
|
|
||||||
|
"${venv_dir}/bin/python" -m pip install \
|
||||||
|
--disable-pip-version-check \
|
||||||
|
--no-input \
|
||||||
|
--requirement "$requirements_file"
|
||||||
|
"${venv_dir}/bin/python" -m pip check
|
||||||
|
cd "$source_dir"
|
||||||
|
"${venv_dir}/bin/python" -m mkdocs build --strict
|
||||||
Executable
+75
@@ -0,0 +1,75 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
readonly deploy_root="/srv/de-roadmap"
|
||||||
|
readonly releases_root="${deploy_root}/releases"
|
||||||
|
readonly keep_releases=3
|
||||||
|
|
||||||
|
if [[ $# -ne 2 ]]; then
|
||||||
|
echo "Usage: $0 SITE_DIR COMMIT_SHA-RUN_ID" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
source_dir=$(realpath "$1")
|
||||||
|
release_id=$2
|
||||||
|
|
||||||
|
if [[ ! $release_id =~ ^[0-9a-f]{40}-[0-9]+$ ]]; then
|
||||||
|
echo "Invalid release id: ${release_id}" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -f "${source_dir}/index.html" ]]; then
|
||||||
|
echo "Built site has no index.html: ${source_dir}" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -d $releases_root || ! -w $releases_root || ! -w $deploy_root ]]; then
|
||||||
|
echo "Deployment directories are missing or not writable" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
readonly release_dir="${releases_root}/${release_id}"
|
||||||
|
readonly staging_dir="${releases_root}/.${release_id}.tmp"
|
||||||
|
readonly next_link="${deploy_root}/.current.${release_id}.tmp"
|
||||||
|
|
||||||
|
if [[ -e $release_dir || -e $staging_dir || -e $next_link ]]; then
|
||||||
|
echo "Release path already exists: ${release_id}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
rm -rf -- "$staging_dir"
|
||||||
|
rm -f -- "$next_link"
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
umask 0022
|
||||||
|
mkdir "$staging_dir"
|
||||||
|
cp -a "${source_dir}/." "$staging_dir/"
|
||||||
|
chmod -R u=rwX,go=rX "$staging_dir"
|
||||||
|
mv "$staging_dir" "$release_dir"
|
||||||
|
|
||||||
|
ln -s "releases/${release_id}" "$next_link"
|
||||||
|
mv -Tf "$next_link" "${deploy_root}/current"
|
||||||
|
|
||||||
|
mapfile -t old_releases < <(
|
||||||
|
find "$releases_root" \
|
||||||
|
-mindepth 1 \
|
||||||
|
-maxdepth 1 \
|
||||||
|
-type d \
|
||||||
|
-regextype posix-extended \
|
||||||
|
-regex '.*/[0-9a-f]{40}-[0-9]+' \
|
||||||
|
-printf '%T@ %f\n' \
|
||||||
|
| sort -nr \
|
||||||
|
| awk -v keep="$keep_releases" 'NR > keep { print $2 }'
|
||||||
|
)
|
||||||
|
|
||||||
|
for old_release in "${old_releases[@]}"; do
|
||||||
|
if [[ $old_release =~ ^[0-9a-f]{40}-[0-9]+$ && $old_release != "$release_id" ]]; then
|
||||||
|
rm -rf -- "${releases_root:?}/${old_release}"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
trap - EXIT
|
||||||
|
echo "Published release ${release_id}"
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
name: Deploy MkDocs to VPS
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: site-production
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: de-roadmap-host
|
||||||
|
timeout-minutes: 15
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Check out the triggering commit
|
||||||
|
env:
|
||||||
|
COMMIT_SHA: ${{ gitea.sha }}
|
||||||
|
REPOSITORY_URL: ${{ gitea.server_url }}/${{ gitea.repository }}.git
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
[[ "$COMMIT_SHA" =~ ^[0-9a-f]{40}$ ]]
|
||||||
|
test ! -e source
|
||||||
|
mkdir source
|
||||||
|
git -C source init .
|
||||||
|
git -C source remote add origin "$REPOSITORY_URL"
|
||||||
|
git -C source fetch --no-tags --depth=1 origin "$COMMIT_SHA"
|
||||||
|
test "$(git -C source rev-parse FETCH_HEAD)" = "$COMMIT_SHA"
|
||||||
|
git -C source -c advice.detachedHead=false checkout --detach FETCH_HEAD
|
||||||
|
|
||||||
|
- name: Build the site strictly
|
||||||
|
run: source/.gitea/scripts/build-site.sh source
|
||||||
|
|
||||||
|
- name: Publish the complete release atomically
|
||||||
|
env:
|
||||||
|
RELEASE_ID: ${{ gitea.sha }}-${{ gitea.run_id }}
|
||||||
|
run: source/.gitea/scripts/deploy-site.sh source/site "$RELEASE_ID"
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
name: Check external links
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: "0 6 * * 1" # понедельник 06:00 UTC
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
issues: write
|
||||||
|
actions: write # для keepalive-шага
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
link-check:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Run lychee
|
||||||
|
id: lychee
|
||||||
|
uses: lycheeverse/lychee-action@v2
|
||||||
|
with:
|
||||||
|
# остальные настройки — в lychee.toml в корне репозитория
|
||||||
|
args: --no-progress './**/*.md'
|
||||||
|
fail: false
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
# если открытый issue с меткой link-check уже есть — обновляем его,
|
||||||
|
# а не создаём дубликат каждую неделю
|
||||||
|
- name: Find open link-check issue
|
||||||
|
if: steps.lychee.outputs.exit_code != 0
|
||||||
|
id: issue
|
||||||
|
run: |
|
||||||
|
echo "number=$(gh issue list --repo "$GITHUB_REPOSITORY" --label link-check --state open --json number --jq '.[0].number // empty')" >> "$GITHUB_OUTPUT"
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
|
||||||
|
- name: Create or update issue on broken links
|
||||||
|
if: steps.lychee.outputs.exit_code != 0
|
||||||
|
uses: peter-evans/create-issue-from-file@v5
|
||||||
|
with:
|
||||||
|
title: "Битые внешние ссылки: еженедельная проверка"
|
||||||
|
content-filepath: ./lychee/out.md
|
||||||
|
labels: link-check
|
||||||
|
issue-number: ${{ steps.issue.outputs.number }}
|
||||||
|
|
||||||
|
# GitHub отключает scheduled-workflows после 60 дней без активности
|
||||||
|
# в репозитории; повторное включение сбрасывает таймер
|
||||||
|
- name: Keep scheduled workflow enabled
|
||||||
|
if: always()
|
||||||
|
run: gh api -X PUT "repos/$GITHUB_REPOSITORY/actions/workflows/check-links.yml/enable"
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
@@ -4,9 +4,12 @@
|
|||||||
- Root `README.md` describes the learning roadmap (RU) and serves as the main page of the MkDocs site.
|
- Root `README.md` describes the learning roadmap (RU) and serves as the main page of the MkDocs site.
|
||||||
- `dwh-modeling/` contains the article and demo DWH model; SQL lives in `dwh-modeling/sql` as ordered scripts `01_...sql`–`09_...sql` (07–09 are homework DDL, template and solution).
|
- `dwh-modeling/` contains the article and demo DWH model; SQL lives in `dwh-modeling/sql` as ordered scripts `01_...sql`–`09_...sql` (07–09 are homework DDL, template and solution).
|
||||||
- `postgres-bookings/` is a Dockerized PostgreSQL + demo “bookings” DB; start it first, then apply DWH scripts against the `demo` database.
|
- `postgres-bookings/` is a Dockerized PostgreSQL + demo “bookings” DB; start it first, then apply DWH scripts against the `demo` database.
|
||||||
- `mkdocs.yml` — MkDocs Material config; `docs_dir: .` (repo root = site root). Excluded dirs: `project/`, `postgres-bookings/`, `.github/`, `.claude/`.
|
- `mkdocs.yml` — MkDocs Material config; `docs_dir: .` (repo root = site root). Excluded dirs: `project/`, `postgres-bookings/`, `.gitea/`, `.github/`, `.claude/`.
|
||||||
- `.github/workflows/deploy-site.yml` — CI/CD: push to `main` → build → deploy to GitHub Pages.
|
- `.gitea/workflows/deploy-site.yml` — основной CI/CD: push в `main` → строгая
|
||||||
- `project/` — PRD and ADR (excluded from site).
|
сборка → атомарная публикация на VPS через repository-scoped Gitea Runner.
|
||||||
|
- `.github/workflows/deploy-site.yml` — сохранённый workflow для резервной
|
||||||
|
публикации на GitHub Pages; Gitea его не исполняет.
|
||||||
|
- `project/` — PRD, ADR, and TODO.md (excluded from site). `project/TODO.md` is the prioritized project backlog: check it when planning or proposing work, and mark items done there when you complete them.
|
||||||
|
|
||||||
## Build, Test, and Development Commands
|
## Build, Test, and Development Commands
|
||||||
- Start demo Postgres:
|
- Start demo Postgres:
|
||||||
@@ -52,15 +55,17 @@ All `.md` files MUST render correctly on both GitHub and the MkDocs Material sit
|
|||||||
- Avoid: em-dash `—`, en-dash `–` — slug behavior differs between GitHub and MkDocs.
|
- Avoid: em-dash `—`, en-dash `–` — slug behavior differs between GitHub and MkDocs.
|
||||||
|
|
||||||
## MkDocs Site Commands
|
## MkDocs Site Commands
|
||||||
|
- Initialize or update the persistent local environment:
|
||||||
|
`python3 -m venv "${HOME}/.cache/de-roadmap-mkdocs" && "${HOME}/.cache/de-roadmap-mkdocs/bin/python" -m pip install -r .gitea/requirements-site.txt`
|
||||||
- Local preview (user starts, ask user to run via `!`):
|
- Local preview (user starts, ask user to run via `!`):
|
||||||
`uv run --with 'mkdocs-material==9.6.14' --with 'mkdocs-same-dir==0.1.3' mkdocs serve`
|
`"${HOME}/.cache/de-roadmap-mkdocs/bin/python" -m mkdocs serve`
|
||||||
- Build with strict validation (catches broken links/anchors):
|
- Build with strict validation (catches broken links/anchors):
|
||||||
`uv run --with 'mkdocs-material==9.6.14' --with 'mkdocs-same-dir==0.1.3' mkdocs build --strict`
|
`"${HOME}/.cache/de-roadmap-mkdocs/bin/python" -m mkdocs build --strict`
|
||||||
- Visual check via Playwright (when `mkdocs serve` is running on port 8000):
|
- Visual check via Playwright (when `mkdocs serve` is running on port 8000):
|
||||||
`npx playwright screenshot --viewport-size='1280,800' 'http://127.0.0.1:8000/#anchor' /path/to/screenshot.png`
|
`npx playwright screenshot --viewport-size='1280,800' 'http://127.0.0.1:8000/#anchor' /path/to/screenshot.png`
|
||||||
Then read the screenshot with the Read tool to inspect rendering. Use `--viewport-size='1280,2000'` for tall pages.
|
Then read the screenshot with the Read tool to inspect rendering. Use `--viewport-size='1280,2000'` for tall pages.
|
||||||
- Kill stuck dev server: `lsof -ti :8000 | xargs kill`
|
- Kill stuck dev server: `lsof -ti :8000 | xargs kill`
|
||||||
- Site URL: `https://dementev-dev.github.io/de-roadmap/`
|
- Site URL: `https://de.dementev.space/` (старый адрес `https://dementev-dev.github.io/de-roadmap/` отдаёт 404)
|
||||||
|
|
||||||
## Testing Guidelines
|
## Testing Guidelines
|
||||||
- There is no dedicated test framework; treat SQL scripts as executable documentation.
|
- There is no dedicated test framework; treat SQL scripts as executable documentation.
|
||||||
@@ -75,5 +80,7 @@ Pull requests should focus on one topic, include a brief context, list of change
|
|||||||
|
|
||||||
## Security & Configuration Tips
|
## Security & Configuration Tips
|
||||||
- Do not commit personal `.env` files or credentials; use local overrides only.
|
- Do not commit personal `.env` files or credentials; use local overrides only.
|
||||||
|
- Gitea Runner работает в host mode: не расширяйте его scope, не добавляйте
|
||||||
|
пользователя `gitea-runner` в `sudo` или `docker` и не выдавайте ему запись
|
||||||
|
вне `/var/lib/gitea-runner` и `/srv/de-roadmap`.
|
||||||
- Demo credentials and ports in `postgres-bookings` are for local training only—never reuse them in shared or production environments.
|
- Demo credentials and ports in `postgres-bookings` are for local training only—never reuse them in shared or production environments.
|
||||||
|
|
||||||
|
|||||||
@@ -12,10 +12,10 @@
|
|||||||
|
|
||||||
### Оглавление
|
### Оглавление
|
||||||
|
|
||||||
- [Основные знания](#основные-знания) — Git, SQL, Python, методологии, Docker
|
- [Основные знания](#основные-знания) — Linux, Git, SQL, Python, методологии, Docker
|
||||||
- [Практика и инструменты](#практика-и-инструменты) — Airflow, Greenplum, курсовая работа
|
- [Практика и инструменты](#практика-и-инструменты) — Airflow, Greenplum, курсовая работа
|
||||||
- [Карьера и менторство](#карьера-и-менторство) — резюме, собеседования, испытательный срок
|
- [Карьера и менторство](#карьера-и-менторство) — резюме, собеседования, испытательный срок
|
||||||
- [Расширенные навыки](#расширенные-навыки) — ClickHouse, Streaming, Lakehouse, dbt
|
- [Расширенные навыки](#расширенные-навыки) — Streaming, ClickHouse, Lakehouse, dbt
|
||||||
- [Софт скиллы](#софт-скиллы)
|
- [Софт скиллы](#софт-скиллы)
|
||||||
- [Дополнительные материалы](#дополнительные-материалы)
|
- [Дополнительные материалы](#дополнительные-материалы)
|
||||||
|
|
||||||
@@ -33,6 +33,16 @@
|
|||||||
|
|
||||||
### Git и базовые инструменты
|
### Git и базовые инструменты
|
||||||
|
|
||||||
|
#### Linux и терминал
|
||||||
|
|
||||||
|
Командная строка — рабочее место дата-инженера: docker, psql, git, подключение к серверам живут именно там. Отдельная практика не нужна — все стенды этого роадмапа консольные, команды закрепятся сами. На Windows поставьте [WSL](https://learn.microsoft.com/ru-ru/windows/wsl/install) — полноценный Linux внутри Windows.
|
||||||
|
|
||||||
|
- [Основы Linux для начинающих за 1.5 часа - Youtube](https://www.youtube.com/watch?v=Be6tB59b7D0) — что такое терминал, навигация, файлы, права, ssh: мягкий вход перед статьями
|
||||||
|
- [Linux: Файлы, навигация и поиск - Habr](https://habr.com/ru/articles/1003550/) — перемещение по каталогам, чтение файлов и логов: less, tail, grep
|
||||||
|
- [Права доступа к файлам и папкам в Linux - FirstVDS](https://firstvds.ru/technology/linux-permissions) — rwx, chmod, chown
|
||||||
|
- [SSH для начинающих - Cloud.ru](https://cloud.ru/blog/ssh-dlya-nachinayuschikh) — подключение к удалённой машине
|
||||||
|
- [Основы командной строки - Hexlet](https://ru.hexlet.io/programs/cli-basics) — опционально: бесплатный интерактивный курс с терминалом прямо в браузере; достаточно уроков про навигацию, grep и права доступа
|
||||||
|
|
||||||
#### База по Git
|
#### База по Git
|
||||||
Что такое контроль версий, когда используется, ПОЧЕМУ и как мы в обучении будем использовать.
|
Что такое контроль версий, когда используется, ПОЧЕМУ и как мы в обучении будем использовать.
|
||||||
Как создать репозиторий на GitHub, сохранять в нем изменения.
|
Как создать репозиторий на GitHub, сохранять в нем изменения.
|
||||||
@@ -51,6 +61,8 @@
|
|||||||
|
|
||||||
**Когда блок Git и базовые инструменты считаем пройденным:**
|
**Когда блок Git и базовые инструменты считаем пройденным:**
|
||||||
|
|
||||||
|
- ориентируетесь в терминале: перемещаетесь по каталогам и находите нужное в логах (grep, tail, less);
|
||||||
|
- понимаете права файлов (rwx, chmod) и знаете, как подключиться к серверу по ssh;
|
||||||
- вы уверенно создаёте репозиторий, коммитите изменения и отправляете их на GitHub;
|
- вы уверенно создаёте репозиторий, коммитите изменения и отправляете их на GitHub;
|
||||||
- имеете представление о работе с ветками: создание, переключение, что такое merge/PR и разруливание простых конфликтов;
|
- имеете представление о работе с ветками: создание, переключение, что такое merge/PR и разруливание простых конфликтов;
|
||||||
- оформляете базовую документацию в Markdown (README, заголовки, списки, ссылки, кодовые блоки).
|
- оформляете базовую документацию в Markdown (README, заголовки, списки, ссылки, кодовые блоки).
|
||||||
@@ -65,15 +77,16 @@ SQL и моделирование данных специально идут р
|
|||||||
Целевой уровень знания SQL - Live кодинг на собесе. Проверяем на первом мок-интервью
|
Целевой уровень знания SQL - Live кодинг на собесе. Проверяем на первом мок-интервью
|
||||||
|
|
||||||
**CTE**
|
**CTE**
|
||||||
- Зачем нам CTE: [Getting started with CTEs | dbt Labs](https://www.getdbt.com/blog/getting-started-with-cte)
|
|
||||||
- Подробнее про синтаксис: [PostgreSQL : Документация: 17: 7.8. Запросы WITH (Общие табличные выражения) : Компания Postgres Professional](https://postgrespro.ru/docs/postgresql/17/queries-with)
|
- Зачем нам CTE: [Getting started with CTEs | dbt Labs](https://www.getdbt.com/blog/getting-started-with-cte)
|
||||||
|
- Подробнее про синтаксис: [PostgreSQL : Документация: 17: 7.8. Запросы WITH (Общие табличные выражения) : Компания Postgres Professional](https://postgrespro.ru/docs/postgresql/17/queries-with)
|
||||||
|
|
||||||
Для дальнейшей тренировки и поддержания уровня можно использовать [Database - LeetCode](https://leetcode.com/problem-list/database/). Хорошая подборка задачек: [SQL 50 - Study Plan - LeetCode](https://leetcode.com/studyplan/top-sql-50/)
|
Для дальнейшей тренировки и поддержания уровня можно использовать [Database - LeetCode](https://leetcode.com/problem-list/database/). Хорошая подборка задачек: [SQL 50 - Study Plan - LeetCode](https://leetcode.com/studyplan/top-sql-50/)
|
||||||
|
|
||||||
#### Повышение знаний SQL
|
#### Повышение знаний SQL
|
||||||
|
|
||||||
Смотрим курс от Postgres Pro [DEV1](https://postgrespro.ru/education/courses/DEV1)
|
Смотрим курс от Postgres Pro [DEV1](https://postgrespro.ru/education/courses/DEV1)
|
||||||
Темы - от "Введение" до "SQL" включительно, "Управление доступом", "Резервное копирование". Для лучшего усваивания материала проделываем все примеры и домашние задания из конспектов лекция.
|
Темы - от "Введение" до "SQL" включительно, "Управление доступом", "Резервное копирование". Для лучшего усваивания материала проделываем все примеры и домашние задания из конспектов лекций.
|
||||||
С темой "PL/pgSQL" можно ознакомиться обзорно.
|
С темой "PL/pgSQL" можно ознакомиться обзорно.
|
||||||
|
|
||||||
Для развития навыков инженера будет полезно лабораторные работы делать не в виртуальной машине, а в docker контейнере. Предложенный (не обязательный) вариант - в каталоге `postgres-bookings` репозитория.
|
Для развития навыков инженера будет полезно лабораторные работы делать не в виртуальной машине, а в docker контейнере. Предложенный (не обязательный) вариант - в каталоге `postgres-bookings` репозитория.
|
||||||
@@ -134,7 +147,8 @@ SQL и моделирование данных специально идут р
|
|||||||
- [Работа с файлами в формате CSV, JSON, YAML](https://pyneng.readthedocs.io/ru/latest/book/17_serialization/index.html)
|
- [Работа с файлами в формате CSV, JSON, YAML](https://pyneng.readthedocs.io/ru/latest/book/17_serialization/index.html)
|
||||||
- [Итераторы, итерируемые объекты и генераторы](https://pyneng.readthedocs.io/ru/latest/book/13_iterator_generator/index.html)
|
- [Итераторы, итерируемые объекты и генераторы](https://pyneng.readthedocs.io/ru/latest/book/13_iterator_generator/index.html)
|
||||||
- [Декораторы Python: пошаговое руководство](https://habr.com/ru/companies/otus/articles/727590/)
|
- [Декораторы Python: пошаговое руководство](https://habr.com/ru/companies/otus/articles/727590/)
|
||||||
- Работа с датой/временем: https://django.fun/docs/python/3.10/library/datetime/
|
- Работа с датой/временем: [официальная документация по модулю datetime](https://docs.python.org/3/library/datetime.html)
|
||||||
|
- [Сложность алгоритмов. Разбор Big O](https://habr.com/ru/articles/782608/) — короткий материал, чтобы понимать O(n) vs O(n²) на собеседованиях и в коде
|
||||||
- ООП
|
- ООП
|
||||||
- [Tproger: «ООП простыми словами»](https://tproger.ru/experts/oop-in-simple-words)
|
- [Tproger: «ООП простыми словами»](https://tproger.ru/experts/oop-in-simple-words)
|
||||||
- Введение в [ООП](https://metanit.com/python/tutorial/7.1.php)
|
- Введение в [ООП](https://metanit.com/python/tutorial/7.1.php)
|
||||||
@@ -145,7 +159,7 @@ SQL и моделирование данных специально идут р
|
|||||||
- [SkillFactory: Виртуальные окружения в Python](https://blog.skillfactory.ru/venv-virtualnoe-okruzhenie-v-python/)
|
- [SkillFactory: Виртуальные окружения в Python](https://blog.skillfactory.ru/venv-virtualnoe-okruzhenie-v-python/)
|
||||||
- Jupyter Lab
|
- Jupyter Lab
|
||||||
- [Блог Практикума: «Что такое Jupyter Notebook: как установить и открыть»](https://practicum.yandex.ru/blog/chto-takoe-jupyter-notebook/)
|
- [Блог Практикума: «Что такое Jupyter Notebook: как установить и открыть»](https://practicum.yandex.ru/blog/chto-takoe-jupyter-notebook/)
|
||||||
- Готовая реализация Jupyter Lab, включающая в себя Spark, в Docker: https://github.com/dementev-dev/jupyter-spark-docker
|
- Готовый Docker-образ Jupyter Lab со Spark: [Jupyter Docker Stacks, pyspark-notebook](https://jupyter-docker-stacks.readthedocs.io/en/latest/using/selecting.html#jupyter-pyspark-notebook)
|
||||||
- Pandas
|
- Pandas
|
||||||
- [GeeksforGeeks: “Why Pandas is Used in Python”](https://www.geeksforgeeks.org/pandas/why-pandas-is-used-in-python/)
|
- [GeeksforGeeks: “Why Pandas is Used in Python”](https://www.geeksforgeeks.org/pandas/why-pandas-is-used-in-python/)
|
||||||
- [Skillbox: «Для чего нужна библиотека Pandas»](https://skillbox.ru/media/code/rabotaem-s-pandas-osnovnye-ponyatiya-i-realnye-dannye/)
|
- [Skillbox: «Для чего нужна библиотека Pandas»](https://skillbox.ru/media/code/rabotaem-s-pandas-osnovnye-ponyatiya-i-realnye-dannye/)
|
||||||
@@ -199,13 +213,15 @@ Agile — это не метод, а философия. Scrum и Kanban — э
|
|||||||
|
|
||||||
##### 1. Jira (Мировой стандарт)
|
##### 1. Jira (Мировой стандарт)
|
||||||
Самый популярный инструмент. Скорее всего, вы столкнетесь именно с ним.
|
Самый популярный инструмент. Скорее всего, вы столкнетесь именно с ним.
|
||||||
|
|
||||||
* [Как работать с Jira на реальных проектах](https://www.youtube.com/watch?v=oPgm-fsHVfM) (15 мин) — *Отличное видео, где показывают базу: как создать задачу, как перетащить её по доске (Kanban) и что писать в комментариях. Смотреть с 04:00, где начинается практика.*
|
* [Как работать с Jira на реальных проектах](https://www.youtube.com/watch?v=oPgm-fsHVfM) (15 мин) — *Отличное видео, где показывают базу: как создать задачу, как перетащить её по доске (Kanban) и что писать в комментариях. Смотреть с 04:00, где начинается практика.*
|
||||||
* [Создание и настройка Scrum-досок в JIRA](https://www.youtube.com/watch?v=u-u8NRyApUs) — *Если хотите увидеть, как выглядит Спринт и Бэклог изнутри.*
|
* [Создание и настройка Scrum-досок в JIRA](https://www.youtube.com/watch?v=u-u8NRyApUs) — *Если хотите увидеть, как выглядит Спринт и Бэклог изнутри.*
|
||||||
|
|
||||||
##### 2. Yandex Tracker (Российский стандарт)
|
##### 2. Yandex Tracker (Российский стандарт)
|
||||||
Активно внедряется в крупных компаниях РФ. Логика та же, но интерфейс другой.
|
Активно внедряется в крупных компаниях РФ. Логика та же, но интерфейс другой.
|
||||||
|
|
||||||
* [Начало работы в Яндекс.Трекере](https://www.youtube.com/watch?v=pdlYiijjn70) (3 мин) — *Супер-короткий официальный гайд. За 3 минуты показывают всё: очереди, доски, карточки.*
|
* [Начало работы в Яндекс.Трекере](https://www.youtube.com/watch?v=pdlYiijjn70) (3 мин) — *Супер-короткий официальный гайд. За 3 минуты показывают всё: очереди, доски, карточки.*
|
||||||
* [Настройка процесса разработки в Tracker](https://www.youtube.com/watch?v=EdKlYJR2ph0&t=397s)** (c 06:37) — *Более глубокий разбор: как выглядит очередь задач разработчика и жизненный цикл тикета.*
|
* [Настройка процесса разработки в Tracker](https://www.youtube.com/watch?v=EdKlYJR2ph0&t=397s) (c 06:37) — *Более глубокий разбор: как выглядит очередь задач разработчика и жизненный цикл тикета.*
|
||||||
|
|
||||||
> **💡 Совет:**
|
> **💡 Совет:**
|
||||||
> Не бойтесь кнопок. Главное правило любого трекера: **«Взял задачу в работу — переведи статус в In Progress»**. Это сигнал команде, что вы заняты и вас лучше не отвлекать.
|
> Не бойтесь кнопок. Главное правило любого трекера: **«Взял задачу в работу — переведи статус в In Progress»**. Это сигнал команде, что вы заняты и вас лучше не отвлекать.
|
||||||
@@ -234,7 +250,7 @@ Agile — это не метод, а философия. Scrum и Kanban — э
|
|||||||
|
|
||||||
#### Docker
|
#### Docker
|
||||||
|
|
||||||
- Курс https://karpov.courses/docker
|
- Курс [Docker от karpov.courses](https://karpov.courses/docker)
|
||||||
|
|
||||||
Основное предназначение для нас - учебные стенды, где мы разбираем и тренируемся с разными технологиями. На работе - иногда пригождается. На собесах спрашивают редко.
|
Основное предназначение для нас - учебные стенды, где мы разбираем и тренируемся с разными технологиями. На работе - иногда пригождается. На собесах спрашивают редко.
|
||||||
|
|
||||||
@@ -267,7 +283,7 @@ Apache Airflow — инструмент для оркестрации ETL-про
|
|||||||
|
|
||||||
Материалы:
|
Материалы:
|
||||||
|
|
||||||
- [Учебник по Airflow](https://github.com/dementev-dev/airflow-manual)
|
- [Учебник по Airflow](https://git.dementev.space/ddmitry/airflow-manual)
|
||||||
|
|
||||||
**Когда блок Airflow считаем пройденным:**
|
**Когда блок Airflow считаем пройденным:**
|
||||||
|
|
||||||
@@ -297,14 +313,10 @@ Apache Airflow — инструмент для оркестрации ETL-про
|
|||||||
**Курс Yandex по Greenplum** — основной учебный курс, рекомендуется пройти целиком:
|
**Курс Yandex по Greenplum** — основной учебный курс, рекомендуется пройти целиком:
|
||||||
|
|
||||||
- [Бесплатный курс Yandex Cloud по Greenplum](https://yandex.cloud/ru/training/greenplum)
|
- [Бесплатный курс Yandex Cloud по Greenplum](https://yandex.cloud/ru/training/greenplum)
|
||||||
- Практику по курсу удобно делать на стенде [airflow-dwh-gp-lab](https://github.com/dementev-dev/airflow-greenplum) — `make up` поднимает рабочий Greenplum с PXF, не нужен облачный кластер.
|
- Практику по курсу удобно делать на стенде [airflow-dwh-gp-lab](https://git.dementev.space/ddmitry/airflow-greenplum) — `make up` поднимает рабочий Greenplum с PXF, не нужен облачный кластер.
|
||||||
- Стенд покрывает основные темы курса: типы таблиц (heap / appendonly), политики дистрибуции, сжатие, PXF, анализ планов выполнения (`EXPLAIN`).
|
- Стенд покрывает основные темы курса: типы таблиц (heap / appendonly), политики дистрибуции, сжатие, PXF, анализ планов выполнения (`EXPLAIN`).
|
||||||
- Единственное ограничение: cloud-специфичные темы (тема 2 курса — развёртывание в Yandex Cloud) на локальном стенде не покрыты.
|
- Единственное ограничение: cloud-специфичные темы (тема 2 курса — развёртывание в Yandex Cloud) на локальном стенде не покрыты.
|
||||||
|
|
||||||
Дополнительно:
|
|
||||||
|
|
||||||
- [Учебный курс по Greenplum от datafinder](https://datafinder.ru/products/uchebnyy-kurs-po-greenplum) — отдельные главы для углубления.
|
|
||||||
|
|
||||||
**Когда блок Greenplum считаем пройденным:**
|
**Когда блок Greenplum считаем пройденным:**
|
||||||
|
|
||||||
- вы понимаете, как данные распределяются по сегментам, что такое skew и как его увидеть;
|
- вы понимаете, как данные распределяются по сегментам, что такое skew и как его увидеть;
|
||||||
@@ -314,7 +326,7 @@ Apache Airflow — инструмент для оркестрации ETL-про
|
|||||||
### Курсовая работа
|
### Курсовая работа
|
||||||
Курсовая работа — важный майлстоун роадмапа: ваш первый end-to-end data-проект. После неё у вас есть ключевые технические навыки для старта карьеры в Data Engineering.
|
Курсовая работа — важный майлстоун роадмапа: ваш первый end-to-end data-проект. После неё у вас есть ключевые технические навыки для старта карьеры в Data Engineering.
|
||||||
|
|
||||||
Курсовая выполняется на том же стенде [airflow-dwh-gp-lab](https://github.com/dementev-dev/airflow-greenplum), который вы уже использовали для практики по Greenplum.
|
Курсовая выполняется на том же стенде [airflow-dwh-gp-lab](https://git.dementev.space/ddmitry/airflow-greenplum), который вы уже использовали для практики по Greenplum.
|
||||||
|
|
||||||
**Что внутри:**
|
**Что внутри:**
|
||||||
|
|
||||||
@@ -330,14 +342,6 @@ Apache Airflow — инструмент для оркестрации ETL-про
|
|||||||
- автоматическая проверка (валидационный DAG) проходит без ошибок;
|
- автоматическая проверка (валидационный DAG) проходит без ошибок;
|
||||||
- вы можете на собеседовании за 5–10 минут рассказать архитектуру проекта, его цели и показать ключевые части кода.
|
- вы можете на собеседовании за 5–10 минут рассказать архитектуру проекта, его цели и показать ключевые части кода.
|
||||||
|
|
||||||
### Понятие сложности алгоритмов
|
|
||||||
В Data Engineering редко требуется писать сложные алгоритмы, но важно понимать, как оценивать эффективность кода:
|
|
||||||
|
|
||||||
- в SQL — через объём сканируемых данных, типы JOIN’ов, использование индексов;
|
|
||||||
- в Python — через асимптотику операций с pandas/списками (например, O(n) vs O(n²)).
|
|
||||||
|
|
||||||
Это помогает избегать «тормозящих» решений на собеседованиях и в реальных пайплайнах.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Карьера и менторство
|
## Карьера и менторство
|
||||||
@@ -374,7 +378,6 @@ Apache Airflow — инструмент для оркестрации ETL-про
|
|||||||
#### Поиск работы и собеседования
|
#### Поиск работы и собеседования
|
||||||
|
|
||||||
- [Как подтвердить опыт без трудовой / Хабр против работяг](https://www.youtube.com/watch?v=GHqABzA1zi8)
|
- [Как подтвердить опыт без трудовой / Хабр против работяг](https://www.youtube.com/watch?v=GHqABzA1zi8)
|
||||||
- Видео по прохождению собеседований из сообщества ОМ — *в подготовке*
|
|
||||||
- Практика: мок-собеседования с ментором — тренировка ответов, разбор слабых мест, психологическая подготовка к реальным интервью.
|
- Практика: мок-собеседования с ментором — тренировка ответов, разбор слабых мест, психологическая подготовка к реальным интервью.
|
||||||
|
|
||||||
#### Помощь с прохождением испытательного срока
|
#### Помощь с прохождением испытательного срока
|
||||||
@@ -406,10 +409,6 @@ Apache Airflow — инструмент для оркестрации ETL-про
|
|||||||
Практика ограничивается минимальным рабочим примером (запуск в Docker, простой пайплайн или SQL-модель).
|
Практика ограничивается минимальным рабочим примером (запуск в Docker, простой пайплайн или SQL-модель).
|
||||||
Этого достаточно, чтобы уверенно говорить об инструменте на собеседовании и понимать его место в архитектуре — а всё остальное при необходимости осваивается уже на проекте.
|
Этого достаточно, чтобы уверенно говорить об инструменте на собеседовании и понимать его место в архитектуре — а всё остальное при необходимости осваивается уже на проекте.
|
||||||
|
|
||||||
### ClickHouse
|
|
||||||
Бесплатный курс https://yandex.cloud/ru/training/clickhouse
|
|
||||||
Платный курс [ClickHouse для аналитика – Stepik](https://stepik.org/course/100210/promo?search=6551441002)
|
|
||||||
|
|
||||||
### Streaming (NiFi + Kafka)
|
### Streaming (NiFi + Kafka)
|
||||||
NiFi — визуальный конструктор потоков данных, Kafka — распределённая очередь сообщений. Вместе они закрывают типичный сценарий: принять данные, буферизовать, доставить в хранилище.
|
NiFi — визуальный конструктор потоков данных, Kafka — распределённая очередь сообщений. Вместе они закрывают типичный сценарий: принять данные, буферизовать, доставить в хранилище.
|
||||||
|
|
||||||
@@ -418,17 +417,52 @@ NiFi — визуальный конструктор потоков данных
|
|||||||
- [Apache NiFi с нуля за 3 часа (Youtube-плейлист)](https://youtube.com/playlist?list=PL4MpKy3QjNp_rOEEibc4Ro8UK4g8vLX6_&si=W_hidjHmBOZ_aUfS) — первые 4 видео, дальше — по желанию
|
- [Apache NiFi с нуля за 3 часа (Youtube-плейлист)](https://youtube.com/playlist?list=PL4MpKy3QjNp_rOEEibc4Ro8UK4g8vLX6_&si=W_hidjHmBOZ_aUfS) — первые 4 видео, дальше — по желанию
|
||||||
- [Лучший Гайд по Kafka для Начинающих За 1 Час (Youtube)](https://www.youtube.com/watch?v=hbseyn-CfXY)
|
- [Лучший Гайд по Kafka для Начинающих За 1 Час (Youtube)](https://www.youtube.com/watch?v=hbseyn-CfXY)
|
||||||
|
|
||||||
Практика — на стенде [nifi-kafka-postgres-lab](https://github.com/dementev-dev/nifi-kafka-postgres-lab) (Docker Compose с NiFi, Kafka и Postgres):
|
Практика — на стенде [nifi-kafka-postgres-lab](https://git.dementev.space/ddmitry/nifi-kafka-postgres-lab) (Docker Compose с NiFi, Kafka и Postgres):
|
||||||
|
|
||||||
- настраиваем в NiFi простой генератор данных и поток в Postgres;
|
- настраиваем в NiFi простой генератор данных и поток в Postgres;
|
||||||
- строим поток NiFi → Kafka → NiFi → Postgres.
|
- строим поток NiFi → Kafka → NiFi → Postgres.
|
||||||
|
|
||||||
|
Знакомство с Kafka здесь пригодится и дальше: стенд по ClickHouse в следующей секции принимает данные именно через Kafka.
|
||||||
|
|
||||||
|
### ClickHouse
|
||||||
|
ClickHouse — колоночная СУБД для аналитики на больших объёмах: миллиарды строк, агрегации за секунды. В российских компаниях это фактический стандарт для витрин, отчётности и продуктовой аналитики, поэтому на собеседованиях тема всплывает часто.
|
||||||
|
|
||||||
|
Материалы:
|
||||||
|
|
||||||
|
- Бесплатный курс [ClickHouse от Yandex Cloud](https://yandex.cloud/ru/training/clickhouse) — берём за основу, в нём много упражнений
|
||||||
|
- Платный курс [ClickHouse для аналитика – Stepik](https://stepik.org/course/100210/promo?search=6551441002)
|
||||||
|
|
||||||
|
Практика:
|
||||||
|
|
||||||
|
- Упражнения курса Яндекса можно выполнять в их облаке (с оплатой за ресурсы) или бесплатно у себя — на учебном кластере [clickhouse-learning-cluster](https://git.dementev.space/ddmitry/clickhouse-learning-cluster): 4 узла ClickHouse в Docker Compose, репликация, шардинг, балансировка через HAProxy.
|
||||||
|
- Следующий шаг — стенд [clickstream-ch-kafka-superset-demo](https://git.dementev.space/ddmitry/clickstream-ch-kafka-superset-demo), имитирующий полноценное аналитическое хранилище на ClickHouse: Kafka, Airflow, дашборды в Superset, мониторинг (Prometheus/Grafana), слои STG → ODS → DDS → DM. Внутри — собственный продвинутый курс «Кликстрим на ClickHouse» с уроками прямо на стенде.
|
||||||
|
|
||||||
### Lakehouse (Spark, Iceberg, Trino)
|
### Lakehouse (Spark, Iceberg, Trino)
|
||||||
|
|
||||||
> Секция в разработке. Lakehouse — отдельное направление в DE, построенное на разделении compute и storage, открытых табличных форматах (Iceberg, Delta) и движках распределённой обработки (Spark, Trino). Этот роадмап фокусируется на классическом DWH-стеке, поэтому полноценный блок пока не готов — ниже только отправные точки для самостоятельного изучения.
|
Lakehouse — архитектурный подход, который соединяет гибкость Data Lake с гарантиями классического DWH.
|
||||||
|
|
||||||
|
В классическом DWH данные и вычисления живут внутри одной СУБД, в её закрытом формате. В Lakehouse они разделены. Данные лежат файлами в дешёвом хранилище (обычно объектном, вроде S3). Открытый табличный формат (Iceberg, Delta, Hudi) добавляет поверх файлов привычные по СУБД вещи: схемы, транзакции, историю изменений. А вычислительные движки (Spark, Trino, Flink и другие) подключаются к данным снаружи — хоть несколько разных к одним и тем же таблицам.
|
||||||
|
|
||||||
|
Роадмап фокусируется на классическом DWH-стеке, поэтому цель здесь — знакомство, но с настоящей практикой: стенд ниже собирает один из типовых наборов этого конструктора.
|
||||||
|
|
||||||
|
Материалы:
|
||||||
|
|
||||||
|
- Введение в тему: [«Как не утонуть в данных: выбираем между DWH, Data Lake и Lakehouse» (Habr, Arenadata)](https://habr.com/ru/companies/arenadata/articles/885722/) — что такое Lakehouse, чем он отличается от классического DWH и Data Lake и зачем появился
|
||||||
- [DataLearn: «Что такое Apache Spark»](https://youtu.be/Tl9YzC-dQLI) — введение в Spark с нуля, ~40 минут
|
- [DataLearn: «Что такое Apache Spark»](https://youtu.be/Tl9YzC-dQLI) — введение в Spark с нуля, ~40 минут
|
||||||
- Стенд для экспериментов: [mini-lakehouse-lab](https://github.com/dementev-dev/mini-lakehouse-lab) (Spark + Iceberg + Trino + MinIO)
|
|
||||||
|
Практика — курс [«Lakehouse без магии»](https://git.dementev.space/ddmitry/mini-lakehouse-lab) на стенде mini-lakehouse-lab (Spark + Iceberg + Trino + MinIO, всё локально в Docker, без облаков и регистраций):
|
||||||
|
|
||||||
|
- 8 модулей на ~12–15 часов самостоятельной работы; в каждом — объяснение, демонстрация, задание и checkpoint;
|
||||||
|
- пайплайн `raw → bronze → silver` на реальном датасете NYC Taxi;
|
||||||
|
- одна таблица из двух движков: запись через Spark, чтение через Trino — и почему это работает без копирования данных;
|
||||||
|
- schema evolution, time travel и обслуживание таблиц (compaction, expire_snapshots) — с параллелями к знакомым VACUUM/REORGANIZE из мира Postgres/Greenplum.
|
||||||
|
|
||||||
|
Глубже про Iceberg (опционально, лучше после практики на стенде):
|
||||||
|
|
||||||
|
- [«Как на самом деле работает Apache Iceberg» — Владимир Озеров, HighLoad Channel (Youtube)](https://www.youtube.com/watch?v=_3fsE2a2FO4)
|
||||||
|
- [Введение в устройство Parquet и Iceberg (Habr, VK Tech)](https://habr.com/ru/companies/vktech/articles/959398/) — подробный и местами непростой разбор форматов изнутри
|
||||||
|
- [Введение в Apache Iceberg: основы, архитектура, как работает](https://ivan-shamaev.ru/apache-iceberg-tutorial-architecture-how-to-work/#__Apache_Iceberg-2)
|
||||||
|
- [Spark + Iceberg in 1 Hour: Memory Tuning, Joins, Partition (Youtube, англ.)](https://www.youtube.com/watch?v=3R-SLYK-P_0)
|
||||||
|
|
||||||
### dbt
|
### dbt
|
||||||
dbt (data build tool) — инструмент для трансформации данных в хранилище.
|
dbt (data build tool) — инструмент для трансформации данных в хранилище.
|
||||||
@@ -470,9 +504,6 @@ dbt (data build tool) — инструмент для трансформации
|
|||||||
- Клон проекта [dementev_dev/sandbox_db_public-форк](https://gitflic.ru/project/dementev_dev/sandbox_db_public-fork)
|
- Клон проекта [dementev_dev/sandbox_db_public-форк](https://gitflic.ru/project/dementev_dev/sandbox_db_public-fork)
|
||||||
- [System Design. Разбор книги "Высоконагруженные приложения". Глава 1 - Youtube](https://www.youtube.com/watch?v=owjrIB_5go8) — отличный видео-конспект первой главы Клеппмана на русском.
|
- [System Design. Разбор книги "Высоконагруженные приложения". Глава 1 - Youtube](https://www.youtube.com/watch?v=owjrIB_5go8) — отличный видео-конспект первой главы Клеппмана на русском.
|
||||||
- [Индексы в БД - Youtube](https://www.youtube.com/watch?v=DyqtBiDrz3g)
|
- [Индексы в БД - Youtube](https://www.youtube.com/watch?v=DyqtBiDrz3g)
|
||||||
- [Spark + Iceberg in 1 Hour - Memory Tuning, Joins, Partition - Youtube](https://www.youtube.com/watch?v=3R-SLYK-P_0)
|
|
||||||
- [Введение в устройство Parquet и Iceberg - habr](https://habr.com/ru/companies/vktech/articles/959398/)
|
|
||||||
- [Введение в Apache Iceberg. Основы, архитектура, как работает?](https://ivan-shamaev.ru/apache-iceberg-tutorial-architecture-how-to-work/#__Apache_Iceberg-2)
|
|
||||||
- [Алгоритмы: теория и практика. Методы – Stepik](https://stepik.org/course/217/info)
|
- [Алгоритмы: теория и практика. Методы – Stepik](https://stepik.org/course/217/info)
|
||||||
- [Алгоритмы: теория и практика. Структуры данных – Stepik](https://stepik.org/course/1547/promo)
|
- [Алгоритмы: теория и практика. Структуры данных – Stepik](https://stepik.org/course/1547/promo)
|
||||||
- [Apache Hadoop для самых маленьких: HDFS, RACK-AWARENESS, репликация и Data Locality - Youtube](https://youtu.be/0fsY5bW2l84)
|
- [Apache Hadoop для самых маленьких: HDFS, RACK-AWARENESS, репликация и Data Locality - Youtube](https://youtu.be/0fsY5bW2l84)
|
||||||
|
|||||||
+13
-12
@@ -126,17 +126,18 @@ erDiagram
|
|||||||
|
|
||||||
Чуть менее «сказочно», чуть более технично.
|
Чуть менее «сказочно», чуть более технично.
|
||||||
|
|
||||||
### 3.1. Hub — сущность и её бизнес-ключ
|
### 3.1. Hub: сущность и её бизнес-ключ
|
||||||
|
|
||||||
**Hub** содержит:
|
**Hub** содержит:
|
||||||
|
|
||||||
* бизнес‑ключ (customer_bk, order_id, contract_number);
|
* бизнес‑ключ (customer_bk, order_id, contract_number);
|
||||||
* техническую информацию:
|
* техническую информацию:
|
||||||
* record_source — из какой системы пришла первая запись;
|
* record_source — из какой системы пришла первая запись;
|
||||||
* load_dttm — когда запись попала в DV;
|
* load_dttm — когда запись попала в DV;
|
||||||
* иногда — хэш бизнес‑ключа (hk_customer).
|
* иногда — хэш бизнес‑ключа (hk_customer).
|
||||||
|
|
||||||
Главные правила:
|
Главные правила:
|
||||||
|
|
||||||
* один бизнес‑ключ — один хаб (одна строка на сущность, без истории);
|
* один бизнес‑ключ — один хаб (одна строка на сущность, без истории);
|
||||||
* хаб не знает про атрибуты (имя, email) — только идентичность.
|
* хаб не знает про атрибуты (имя, email) — только идентичность.
|
||||||
|
|
||||||
@@ -153,7 +154,7 @@ CREATE TABLE hub_customer (
|
|||||||
|
|
||||||
Конкретные типы данных (`BYTEA`, длины `VARCHAR`, детали `hashdiff`) и реализации хэш‑ключей можно не запоминать: на старте важнее понять саму идею — у сущностей есть стабильные ключи, а все изменения атрибутов мы записываем отдельными версиями в сателлитах.
|
Конкретные типы данных (`BYTEA`, длины `VARCHAR`, детали `hashdiff`) и реализации хэш‑ключей можно не запоминать: на старте важнее понять саму идею — у сущностей есть стабильные ключи, а все изменения атрибутов мы записываем отдельными версиями в сателлитах.
|
||||||
|
|
||||||
### 3.2. Link — связи между сущностями
|
### 3.2. Link: связи между сущностями
|
||||||
|
|
||||||
**Link** описывает факт связи, например:
|
**Link** описывает факт связи, например:
|
||||||
|
|
||||||
@@ -178,7 +179,7 @@ CREATE TABLE link_order_customer (
|
|||||||
);
|
);
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3.3. Satellite — атрибуты и история
|
### 3.3. Satellite: атрибуты и история
|
||||||
|
|
||||||
**Satellite** хранит:
|
**Satellite** хранит:
|
||||||
|
|
||||||
@@ -249,7 +250,7 @@ Star Schema]
|
|||||||
* **Raw Vault** — это про приём и хранение данных «как есть», но уже в форме Hub / Link / Satellite.
|
* **Raw Vault** — это про приём и хранение данных «как есть», но уже в форме Hub / Link / Satellite.
|
||||||
* **Business Vault** — это про приведение этих данных в более «деловой» вид: с бизнес-правилами, PIT/Bridge и подготовленными представлениями.
|
* **Business Vault** — это про приведение этих данных в более «деловой» вид: с бизнес-правилами, PIT/Bridge и подготовленными представлениями.
|
||||||
|
|
||||||
### 5.1. Raw Vault — «всё прилетевшее, аккуратно разложенное по ящичкам»
|
### 5.1. Raw Vault: «всё прилетевшее, аккуратно разложенное по ящичкам»
|
||||||
|
|
||||||
Raw DV — первый слой поверх STG / ODS:
|
Raw DV — первый слой поверх STG / ODS:
|
||||||
|
|
||||||
@@ -261,20 +262,20 @@ Raw DV — первый слой поверх STG / ODS:
|
|||||||
|
|
||||||
* минимум бизнес-логики:
|
* минимум бизнес-логики:
|
||||||
|
|
||||||
* никаких правил вроде «клиент активен, если была хотя бы одна покупка за 90 дней»;
|
* никаких правил вроде «клиент активен, если была хотя бы одна покупка за 90 дней»;
|
||||||
* все источники показываются «как есть», только приведены к общим ключам;
|
* все источники показываются «как есть», только приведены к общим ключам;
|
||||||
* структура стабильна: добавился новый источник → появился новый Satellite к тому же Hub.
|
* структура стабильна: добавился новый источник → появился новый Satellite к тому же Hub.
|
||||||
|
|
||||||
### 5.2. Business Vault — «там, где из Lego собирают модули»
|
### 5.2. Business Vault: «там, где из Lego собирают модули»
|
||||||
|
|
||||||
Business Vault (BV) — следующий слой над Raw DV:
|
Business Vault (BV) — следующий слой над Raw DV:
|
||||||
|
|
||||||
* здесь применяются бизнес-правила (что считать активным клиентом, как трактовать статусы);
|
* здесь применяются бизнес-правила (что считать активным клиентом, как трактовать статусы);
|
||||||
* здесь строятся вспомогательные структуры:
|
* здесь строятся вспомогательные структуры:
|
||||||
|
|
||||||
* PIT-таблицы,
|
* PIT-таблицы,
|
||||||
* Bridge-таблицы,
|
* Bridge-таблицы,
|
||||||
* агрегаты и derived-таблицы.
|
* агрегаты и derived-таблицы.
|
||||||
|
|
||||||
Именно из BV чаще всего строятся витрины в формате Звезды, к которым подключаются BI и отчётность.
|
Именно из BV чаще всего строятся витрины в формате Звезды, к которым подключаются BI и отчётность.
|
||||||
|
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ customer_id,status,event_ts,_load_id,_load_ts
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3. Часть 1 — STG → ODS (обязательно)
|
## 3. Часть 1: STG → ODS (обязательно)
|
||||||
|
|
||||||
**Задача:** загрузить CSV в STG и переложить данные в ODS с приведением типов.
|
**Задача:** загрузить CSV в STG и переложить данные в ODS с приведением типов.
|
||||||
|
|
||||||
@@ -145,7 +145,7 @@ ORDER BY customer_id, event_ts;
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 4. Часть 2 — ODS → DDS (SCD Type 2, обязательно)
|
## 4. Часть 2: ODS → DDS (SCD Type 2, обязательно)
|
||||||
|
|
||||||
**Задача:** по событиям в `ods.customer_status` построить измерение `dds.dim_customer_status`, где каждая строка — период действия статуса.
|
**Задача:** по событиям в `ods.customer_status` построить измерение `dds.dim_customer_status`, где каждая строка — период действия статуса.
|
||||||
|
|
||||||
@@ -216,7 +216,7 @@ ORDER BY customer_bk, valid_from;
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 5. Часть 3 — инкрементальная загрузка (по желанию)
|
## 5. Часть 3: инкрементальная загрузка (по желанию)
|
||||||
|
|
||||||
Если хочется потренироваться глубже:
|
Если хочется потренироваться глубже:
|
||||||
|
|
||||||
@@ -241,7 +241,7 @@ cat dwh-modeling/data/customer_status_events_increment.csv | ./postgres-bookings
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 6. Часть 4 — витрина в DM (по желанию)
|
## 6. Часть 4: витрина в DM (по желанию)
|
||||||
|
|
||||||
Опциональное задание для закрепления: собрать небольшую витрину с количеством клиентов по статусам на каждую дату.
|
Опциональное задание для закрепления: собрать небольшую витрину с количеством клиентов по статусам на каждую дату.
|
||||||
|
|
||||||
|
|||||||
+12
-12
@@ -145,7 +145,7 @@ flowchart TD
|
|||||||
|
|
||||||
Давайте проследим, как превращается строка заказа.
|
Давайте проследим, как превращается строка заказа.
|
||||||
|
|
||||||
### **STG (Staging / Bronze)** — «как пришло»
|
### **STG (Staging / Bronze)**: «как пришло»
|
||||||
|
|
||||||
- Таблицы: `stg.orders_raw`, `stg.customers_raw`;
|
- Таблицы: `stg.orders_raw`, `stg.customers_raw`;
|
||||||
- Структура — *точно как в источнике* (может быть `VARCHAR` даже у дат);
|
- Структура — *точно как в источнике* (может быть `VARCHAR` даже у дат);
|
||||||
@@ -158,7 +158,7 @@ flowchart TD
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### **ODS (Operational Data Store / Silver)** — «почистили, но не трогали смысл»
|
### **ODS (Operational Data Store / Silver)**: «почистили, но не трогали смысл»
|
||||||
|
|
||||||
- Таблицы: `ods.orders`, `ods.customers`;
|
- Таблицы: `ods.orders`, `ods.customers`;
|
||||||
- Здесь:
|
- Здесь:
|
||||||
@@ -174,7 +174,7 @@ flowchart TD
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### **DDS (Data Delivery Store / Core / Conformed)** — «интеграция + история»
|
### **DDS (Data Delivery Store / Core / Conformed)**: «интеграция + история»
|
||||||
|
|
||||||
Здесь рождается *единая бизнес-модель*.
|
Здесь рождается *единая бизнес-модель*.
|
||||||
Появляются понятия: **измерения**, **факты**, **суррогатные ключи**, **SCD**.
|
Появляются понятия: **измерения**, **факты**, **суррогатные ключи**, **SCD**.
|
||||||
@@ -199,7 +199,7 @@ flowchart TD
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### **DM (Data Mart / Gold/ «Витрины»)** — «готово к употреблению»
|
### **DM (Data Mart / Gold / «Витрины»)**: «готово к употреблению»
|
||||||
|
|
||||||
Здесь — таблицы и представления для конкретных задач:
|
Здесь — таблицы и представления для конкретных задач:
|
||||||
|
|
||||||
@@ -266,7 +266,7 @@ erDiagram
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### SCD Type 2 — как хранить историю
|
### SCD Type 2: как хранить историю
|
||||||
|
|
||||||
Клиент №101:
|
Клиент №101:
|
||||||
|
|
||||||
@@ -418,7 +418,7 @@ WHERE c.city = 'Москва'
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 3. Data Vault 2.0 — «конструктор Lego» для больших DWH
|
### 3. Data Vault 2.0: «конструктор Lego» для больших DWH
|
||||||
|
|
||||||
*Идея: Дэн Линстедт (Dan Linstedt). Цель — так организовать хранилище, чтобы можно было спокойно добавлять новые источники и хранить историю, не ломая старую модель.*
|
*Идея: Дэн Линстедт (Dan Linstedt). Цель — так организовать хранилище, чтобы можно было спокойно добавлять новые источники и хранить историю, не ломая старую модель.*
|
||||||
|
|
||||||
@@ -468,7 +468,7 @@ Anchor Modeling - ещё более атомарный подход к моде
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Сравнение моделей — наглядно
|
### Сравнение моделей наглядно
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
quadrantChart
|
quadrantChart
|
||||||
@@ -540,7 +540,7 @@ flowchart TD
|
|||||||
|
|
||||||
### Готовые SQL-скрипты
|
### Готовые SQL-скрипты
|
||||||
|
|
||||||
Все необходимые скрипты для построения хранилища находятся в папке [`sql/`](sql/):
|
Все необходимые скрипты для построения хранилища находятся в папке [`sql/`](https://git.dementev.space/ddmitry/de-roadmap/src/branch/main/dwh-modeling/sql):
|
||||||
|
|
||||||
- [`01_ddl_stg-dds.sql`](sql/01_ddl_stg-dds.sql) — создание схем и таблиц (STG, ODS, DDS);
|
- [`01_ddl_stg-dds.sql`](sql/01_ddl_stg-dds.sql) — создание схем и таблиц (STG, ODS, DDS);
|
||||||
- [`02_dml_stg-dds.sql`](sql/02_dml_stg-dds.sql) — первичная загрузка данных и демонстрация SCD2 через полный пересчёт (`full backfill`) из STG;
|
- [`02_dml_stg-dds.sql`](sql/02_dml_stg-dds.sql) — первичная загрузка данных и демонстрация SCD2 через полный пересчёт (`full backfill`) из STG;
|
||||||
@@ -605,7 +605,7 @@ GROUP BY d.date_actual, p.product_name,
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### ✅ Базовые советы — с чего начать, если вы учитесь или делаете первый DWH
|
### ✅ Базовые советы: с чего начать, если вы учитесь или делаете первый DWH
|
||||||
|
|
||||||
1. **Начните с витрины в формате Звезды (Star Schema).**
|
1. **Начните с витрины в формате Звезды (Star Schema).**
|
||||||
— Это просто: одна таблица фактов + несколько «плоских» измерений.
|
— Это просто: одна таблица фактов + несколько «плоских» измерений.
|
||||||
@@ -660,7 +660,7 @@ GROUP BY d.date_actual, p.product_name,
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 📌 Кратко — что выбрать *сегодня*, если вы только учитесь
|
### 📌 Кратко: что выбрать *сегодня*, если вы только учитесь
|
||||||
|
|
||||||
| У вас… | Делайте… |
|
| У вас… | Делайте… |
|
||||||
|--------|----------|
|
|--------|----------|
|
||||||
@@ -791,7 +791,7 @@ SELECT 'OK' WHERE EXISTS (
|
|||||||
|
|
||||||
### Мини-датасет (для практики)
|
### Мини-датасет (для практики)
|
||||||
|
|
||||||
Все данные для практики находятся в папке [`data/`](data/) — тренируйтесь:
|
Все данные для практики находятся в папке [`data/`](https://git.dementev.space/ddmitry/de-roadmap/src/branch/main/dwh-modeling/data) — тренируйтесь:
|
||||||
|
|
||||||
[`customers.csv`](data/customers.csv):
|
[`customers.csv`](data/customers.csv):
|
||||||
```csv
|
```csv
|
||||||
@@ -832,7 +832,7 @@ product_id,valid_from,valid_to,price
|
|||||||
9002,2023-01-01,,50
|
9002,2023-01-01,,50
|
||||||
```
|
```
|
||||||
|
|
||||||
> 📂 Все SQL-скрипты для построения хранилища находятся в папке [`sql/`](sql/).
|
> 📂 Все SQL-скрипты для построения хранилища находятся в папке [`sql/`](https://git.dementev.space/ddmitry/de-roadmap/src/branch/main/dwh-modeling/sql).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
+7
-7
@@ -28,15 +28,15 @@ SCD — это подход к хранению изменений в измер
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3. Типы SCD — простыми словами
|
## 3. Типы SCD простыми словами
|
||||||
|
|
||||||
Существует несколько стандартных стратегий обработки изменений. Рассмотрим самые важные.
|
Существует несколько стандартных стратегий обработки изменений. Рассмотрим самые важные.
|
||||||
|
|
||||||
### **Type 0 — Никогда не меняется**
|
### **Type 0: никогда не меняется**
|
||||||
Атрибут фиксирован навсегда. Например, дата рождения клиента.
|
Атрибут фиксирован навсегда. Например, дата рождения клиента.
|
||||||
Такие поля не требуют специальной обработки — они просто не обновляются.
|
Такие поля не требуют специальной обработки — они просто не обновляются.
|
||||||
|
|
||||||
### **Type 1 — Просто перезаписать**
|
### **Type 1: просто перезаписать**
|
||||||
Вы просто делаете `UPDATE`, и старое значение исчезает.
|
Вы просто делаете `UPDATE`, и старое значение исчезает.
|
||||||
|
|
||||||
✅ Просто.
|
✅ Просто.
|
||||||
@@ -44,7 +44,7 @@ SCD — это подход к хранению изменений в измер
|
|||||||
|
|
||||||
> Подходит, если изменение — это исправление ошибки (например, опечатка в имени).
|
> Подходит, если изменение — это исправление ошибки (например, опечатка в имени).
|
||||||
|
|
||||||
### **Type 2 — Новая строка для новой версии**
|
### **Type 2: новая строка для новой версии**
|
||||||
Каждое изменение порождает **новую строку** в таблице. Старая строка остаётся, но помечается как «устаревшая».
|
Каждое изменение порождает **новую строку** в таблице. Старая строка остаётся, но помечается как «устаревшая».
|
||||||
|
|
||||||
✅ Полная история.
|
✅ Полная история.
|
||||||
@@ -53,7 +53,7 @@ SCD — это подход к хранению изменений в измер
|
|||||||
|
|
||||||
> Это **самый распространённый** подход в аналитике.
|
> Это **самый распространённый** подход в аналитике.
|
||||||
|
|
||||||
### **Type 3 — Добавить колонку «предыдущее значение»**
|
### **Type 3: добавить колонку «предыдущее значение»**
|
||||||
В таблице появляются поля вроде `previous_category`, `category_change_date`.
|
В таблице появляются поля вроде `previous_category`, `category_change_date`.
|
||||||
|
|
||||||
✅ Простая история «до/после».
|
✅ Простая история «до/после».
|
||||||
@@ -61,7 +61,7 @@ SCD — это подход к хранению изменений в измер
|
|||||||
|
|
||||||
> Используется редко, чаще как компромисс в очень простых системах.
|
> Используется редко, чаще как компромисс в очень простых системах.
|
||||||
|
|
||||||
### **Type 4, 5, 6 — Продвинутые гибриды**
|
### **Type 4, 5, 6: продвинутые гибриды**
|
||||||
Эти типы существуют, но **встречаются редко** и почти не используются новичками:
|
Эти типы существуют, но **встречаются редко** и почти не используются новичками:
|
||||||
|
|
||||||
- **Type 4**: история выносится в отдельную таблицу («мини-хранилище» для одного измерения).
|
- **Type 4**: история выносится в отдельную таблицу («мини-хранилище» для одного измерения).
|
||||||
@@ -99,7 +99,7 @@ WHERE customer_id = 1;
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Type 2: сохраняем историю — подробнее
|
### Type 2: сохраняем историю (подробнее)
|
||||||
|
|
||||||
Чтобы хранить историю, мы меняем структуру таблицы. Вот ключевые поля:
|
Чтобы хранить историю, мы меняем структуру таблицы. Вот ключевые поля:
|
||||||
|
|
||||||
|
|||||||
+29
@@ -0,0 +1,29 @@
|
|||||||
|
# Конфигурация lychee — CI-проверка внешних ссылок
|
||||||
|
# Используется в .github/workflows/check-links.yml (подхватывается автоматически)
|
||||||
|
# Локальный запуск: docker run --rm -v "$PWD:/input" -w /input lycheeverse/lychee './**/*.md'
|
||||||
|
|
||||||
|
# Служебные каталоги и каталоги, исключённые из сайта
|
||||||
|
exclude_path = ["project", "site"]
|
||||||
|
|
||||||
|
# Локальные адреса стендов (127.0.0.1, localhost и т.п.)
|
||||||
|
exclude_all_private = true
|
||||||
|
|
||||||
|
exclude = [
|
||||||
|
# Режут ботов и датацентровые IP (проверять вручную из браузера)
|
||||||
|
"^https?://habr\\.com",
|
||||||
|
"^https?://stepik\\.org",
|
||||||
|
"^https?://leetcode\\.com",
|
||||||
|
"^https?://realpython\\.com",
|
||||||
|
# YouTube в CI ненадёжен: 429 на пачку запросов, удалённые видео отдают 200
|
||||||
|
"^https?://(www\\.)?youtube\\.com",
|
||||||
|
"^https?://youtu\\.be",
|
||||||
|
# Telegram отдаёт 200 даже для несуществующих каналов
|
||||||
|
"^https?://t\\.me",
|
||||||
|
]
|
||||||
|
|
||||||
|
# 429 (rate limit) не считаем битой ссылкой
|
||||||
|
accept = ["200..=204", "429"]
|
||||||
|
|
||||||
|
max_retries = 2
|
||||||
|
timeout = 30
|
||||||
|
user_agent = "Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0"
|
||||||
+7
-6
@@ -3,8 +3,8 @@ site_url: https://de.dementev.space/
|
|||||||
site_description: "Роадмап по Data Engineering: SQL, Python, Airflow, Greenplum и далее"
|
site_description: "Роадмап по Data Engineering: SQL, Python, Airflow, Greenplum и далее"
|
||||||
site_author: Dmitry Dementev
|
site_author: Dmitry Dementev
|
||||||
|
|
||||||
repo_url: https://github.com/dementev-dev/de-roadmap
|
repo_url: https://git.dementev.space/ddmitry/de-roadmap
|
||||||
repo_name: dementev-dev/de-roadmap
|
repo_name: ddmitry/de-roadmap
|
||||||
|
|
||||||
docs_dir: .
|
docs_dir: .
|
||||||
site_dir: site
|
site_dir: site
|
||||||
@@ -13,6 +13,7 @@ exclude_docs: |
|
|||||||
project/
|
project/
|
||||||
postgres-bookings/
|
postgres-bookings/
|
||||||
.github/
|
.github/
|
||||||
|
.gitea/
|
||||||
.claude/
|
.claude/
|
||||||
site/
|
site/
|
||||||
AGENTS.md
|
AGENTS.md
|
||||||
@@ -30,7 +31,7 @@ nav:
|
|||||||
- Введение: dwh-modeling/README.md
|
- Введение: dwh-modeling/README.md
|
||||||
- SCD: dwh-modeling/SCD.md
|
- SCD: dwh-modeling/SCD.md
|
||||||
- Data Vault: dwh-modeling/DataVault.md
|
- Data Vault: dwh-modeling/DataVault.md
|
||||||
- "Домашка: STG → DDS → DM": dwh-modeling/Homework_Customer_Status_DDS_DM.md
|
- "Домашка: STG → ODS → DDS → DM": dwh-modeling/Homework_Customer_Status_DDS_DM.md
|
||||||
- Разработка с ИИ:
|
- Разработка с ИИ:
|
||||||
- Введение: ai-dev/README.md
|
- Введение: ai-dev/README.md
|
||||||
- Лучшие практики: ai-dev/best-practice.md
|
- Лучшие практики: ai-dev/best-practice.md
|
||||||
@@ -75,12 +76,12 @@ markdown_extensions:
|
|||||||
|
|
||||||
extra:
|
extra:
|
||||||
social:
|
social:
|
||||||
|
- icon: simple/gitea
|
||||||
|
link: https://git.dementev.space/ddmitry/de-roadmap
|
||||||
|
name: Gitea
|
||||||
- icon: fontawesome/brands/telegram
|
- icon: fontawesome/brands/telegram
|
||||||
link: https://t.me/dementev_dev
|
link: https://t.me/dementev_dev
|
||||||
name: Написать в Telegram
|
name: Написать в Telegram
|
||||||
- icon: fontawesome/brands/github
|
|
||||||
link: https://github.com/dementev-dev/de-roadmap
|
|
||||||
name: GitHub
|
|
||||||
|
|
||||||
plugins:
|
plugins:
|
||||||
- same-dir
|
- same-dir
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
# ADR: Архитектура сайта de-roadmap
|
# ADR: Архитектура сайта de-roadmap
|
||||||
|
|
||||||
> Архитектурный документ. Проектные цели и требования — в [PRD](./PRD.md).
|
> Архитектурный документ. Проектные цели и требования — в [PRD](./PRD.md).
|
||||||
|
> Решения о публикации и custom domain заменены спецификацией
|
||||||
|
> [«Публикация сайта через Gitea Actions и VPS»](./specs/2026-08-04-gitea-vps-site-publishing.md).
|
||||||
|
> Решения о MkDocs, структуре файлов и ссылках остаются актуальными.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
+9
-4
@@ -8,7 +8,10 @@
|
|||||||
|
|
||||||
### Текущее состояние
|
### Текущее состояние
|
||||||
|
|
||||||
Роадмап по Data Engineering живёт как GitHub-репозиторий ([dementev-dev/de-roadmap](https://github.com/dementev-dev/de-roadmap)):
|
Роадмап по Data Engineering живёт как Git-репозиторий. Основной origin
|
||||||
|
размещён в собственной Gitea
|
||||||
|
([ddmitry/de-roadmap](https://git.dementev.space/ddmitry/de-roadmap)), а
|
||||||
|
GitHub Pages сохраняется как резерв после восстановления доступа к GitHub:
|
||||||
|
|
||||||
- Основной контент — монолитный `README.md` (~700 строк) с полным учебным планом.
|
- Основной контент — монолитный `README.md` (~700 строк) с полным учебным планом.
|
||||||
- Дополнительные материалы — в подпапках (`dwh-modeling/`, `postgres-bookings/`): теория DWH-моделирования, SCD, Data Vault, домашние задания, скрипты.
|
- Дополнительные материалы — в подпапках (`dwh-modeling/`, `postgres-bookings/`): теория DWH-моделирования, SCD, Data Vault, домашние задания, скрипты.
|
||||||
@@ -74,7 +77,8 @@ GitHub README — рабочий, но не презентабельный фо
|
|||||||
**Деплой:**
|
**Деплой:**
|
||||||
|
|
||||||
- [x] Автоматическая сборка и публикация при пуше в `main`.
|
- [x] Автоматическая сборка и публикация при пуше в `main`.
|
||||||
- [x] Бесплатный хостинг (GitHub Pages).
|
- [x] Публикация без дополнительных расходов: собственная VPS как основной
|
||||||
|
контур, GitHub Pages как резерв.
|
||||||
|
|
||||||
**Совместимость с репо:**
|
**Совместимость с репо:**
|
||||||
|
|
||||||
@@ -84,7 +88,8 @@ GitHub README — рабочий, но не презентабельный фо
|
|||||||
|
|
||||||
### 4.2. Желательные (Спринт 2+)
|
### 4.2. Желательные (Спринт 2+)
|
||||||
|
|
||||||
- [x] Кастомный домен: `de.dementev.space` (подключён 2026-03-27).
|
- [x] Кастомный домен: `de.dementev.space` (подключён 2026-03-27, переведён на
|
||||||
|
VPS 2026-08-05).
|
||||||
- [x] Тёмная тема (переключатель light/dark).
|
- [x] Тёмная тема (переключатель light/dark).
|
||||||
- [x] Сворачиваемые блоки (`<details>`) — точечно, для подсказок/решений в домашках (2026-03-29).
|
- [x] Сворачиваемые блоки (`<details>`) — точечно, для подсказок/решений в домашках (2026-03-29).
|
||||||
- [x] Кнопка «Написать в Telegram» — floating-кнопка + иконки в футере (2026-03-29).
|
- [x] Кнопка «Написать в Telegram» — floating-кнопка + иконки в футере (2026-03-29).
|
||||||
@@ -158,7 +163,7 @@ GitHub README — рабочий, но не презентабельный фо
|
|||||||
| Кириллические якоря рендерятся по-разному | Средняя | Среднее | Тестирование конкретного генератора; при необходимости — латинские id |
|
| Кириллические якоря рендерятся по-разному | Средняя | Среднее | Тестирование конкретного генератора; при необходимости — латинские id |
|
||||||
| Генератор сайта перестаёт поддерживаться | Низкая | Среднее | Контент в plain Markdown — миграция на другой генератор за день |
|
| Генератор сайта перестаёт поддерживаться | Низкая | Среднее | Контент в plain Markdown — миграция на другой генератор за день |
|
||||||
| Накладные расходы на поддержку растут | Низкая | Среднее | Принцип «сайт опционален»: если мешает — удаляем конфиг, репо работает |
|
| Накладные расходы на поддержку растут | Низкая | Среднее | Принцип «сайт опционален»: если мешает — удаляем конфиг, репо работает |
|
||||||
| GitHub Pages ограничения (bandwidth, размер) | Очень низкая | Низкое | Для статического сайта с текстом — не актуально |
|
| VPS временно недоступна | Низкая | Высокое | Опубликованный сайт не зависит от Gitea; GitHub Pages сохраняется как ручной резерв |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
+52
-7
@@ -1,17 +1,62 @@
|
|||||||
# TODO: общие задачи проекта
|
# TODO: общие задачи проекта
|
||||||
|
|
||||||
## Контент
|
Приоритеты: P1 — делаем в первую очередь; P2 — полезно, когда дойдут руки; P3 — идеи под вопросом.
|
||||||
|
|
||||||
|
## P2
|
||||||
|
|
||||||
- [ ] **Перенос учебника Airflow в de-roadmap.**
|
- [ ] **Перенос учебника Airflow в de-roadmap.**
|
||||||
Перенести 9 глав учебника из `airflow-manual` в `airflow/` (de-roadmap).
|
Перенести 9 глав учебника из `airflow-manual` в `airflow/` (de-roadmap).
|
||||||
В `airflow-manual` оставить только стенд (`airflow-docker/`).
|
В `airflow-manual` оставить только стенд (`airflow-docker/`).
|
||||||
Добавить навигацию в `mkdocs.yml`, обеспечить dual-compatible links.
|
Добавить навигацию в `mkdocs.yml`, обеспечить dual-compatible links.
|
||||||
|
|
||||||
- [ ] **Убрать раздел «Понятие сложности алгоритмов» из README.**
|
- [ ] **Добавить счётчик Google Analytics** на сайт (MkDocs Material
|
||||||
Раздел поверхностный и не самостоятелен. План:
|
поддерживает GA через `extra.analytics` в `mkdocs.yml`).
|
||||||
- Упоминание асимптотики (O(n) vs O(n²), pandas/списки) перенести в раздел Python.
|
|
||||||
- По SQL: либо короткая заметка про планы запросов, либо просто сослаться на курс QPT от Postgres Pro (он уже упомянут в разделе SQL) и не дублировать.
|
|
||||||
|
|
||||||
## Сайт
|
- [ ] **Ориентиры трудозатрат по блокам.**
|
||||||
|
Одна строка на блок («~N часов»), по образцу курса Lakehouse (~12–15 часов).
|
||||||
|
Менти всегда спрашивают «сколько займёт»; ориентир защищает от провала
|
||||||
|
в практику на месяцы.
|
||||||
|
|
||||||
- [ ] **Иконки/бейджи статуса разделов** (пройден / в процессе / не начат) — декоративные, без бэкенда.
|
- [ ] **Шаблон прогресса менти.**
|
||||||
|
Файл-чеклист по критериям «когда блок считаем пройденным»; менти копирует
|
||||||
|
в свой форк и ведёт коммитами. Живая практика Git с первой недели +
|
||||||
|
прозрачный прогресс для ментора.
|
||||||
|
|
||||||
|
## P3
|
||||||
|
|
||||||
|
- [ ] **Свой dbt-стенд.**
|
||||||
|
Сейчас практика dbt — на чужом jaffle-shop; единственная секция без
|
||||||
|
собственного стенда. Вариант: dbt-модели поверх postgres-bookings или
|
||||||
|
clickstream-стенда.
|
||||||
|
|
||||||
|
- [ ] **Абзац про Data Quality в курсовой.**
|
||||||
|
Валидационный DAG курсовой — это и есть DQ на практике; добавить абзац,
|
||||||
|
как об этом говорить на собеседовании. Новая секция не нужна.
|
||||||
|
|
||||||
|
- [ ] **Иконки/бейджи статуса разделов** (пройден / в процессе / не начат) —
|
||||||
|
декоративные, без бэкенда. Сомнение: статус у каждого менти свой,
|
||||||
|
пересекается с идеей шаблона прогресса — возможно, отпадёт.
|
||||||
|
|
||||||
|
## Сделано
|
||||||
|
|
||||||
|
- [x] **Публикация сайта перенесена на Gitea Actions и VPS** —
|
||||||
|
2026-08-05: настроены repository-scoped host runner, строгая сборка MkDocs,
|
||||||
|
атомарные релизы, nginx и TLS для `de.dementev.space`; GitHub Pages сохранён
|
||||||
|
как неактивный резерв.
|
||||||
|
|
||||||
|
- [x] **Подраздел «Linux и терминал» в блоке базовых инструментов** —
|
||||||
|
2026-07-12: видео-интро («Девопс на троечку», покрытие проверено по
|
||||||
|
субтитрам) + три статьи (навигация и grep — habr, права — FirstVDS,
|
||||||
|
ssh — Cloud.ru) + опциональный интерактивный курс Hexlet, примечание про
|
||||||
|
WSL для Windows, два новых критерия готовности блока, Linux добавлен
|
||||||
|
в строку оглавления. Отдельной практики нет — ею служат стенды.
|
||||||
|
|
||||||
|
- [x] **CI-проверка внешних ссылок** — 2026-07-12: `lychee.toml` + workflow
|
||||||
|
`check-links.yml` (еженедельно по понедельникам, при битых ссылках создаёт
|
||||||
|
issue). Игнор-лист: habr, stepik, leetcode, realpython (режут ботов),
|
||||||
|
YouTube и t.me (проверка ненадёжна). Попутно исправлена битая ссылка на
|
||||||
|
русские доки Python в README (перевод `/ru/` на docs.python.org умер целиком).
|
||||||
|
|
||||||
|
- [x] **Убрать раздел «Понятие сложности алгоритмов» из README** —
|
||||||
|
2026-07-12, коммит `6f61c0e`: асимптотика перенесена в Python ссылкой
|
||||||
|
на разбор Big O (habr), по SQL — без дублирования, тему покрывает QPT.
|
||||||
|
|||||||
@@ -0,0 +1,283 @@
|
|||||||
|
# Эксплуатация публикации `de.dementev.space`
|
||||||
|
|
||||||
|
Этот runbook реализует спецификацию
|
||||||
|
[`2026-08-04-gitea-vps-site-publishing.md`](../../specs/2026-08-04-gitea-vps-site-publishing.md).
|
||||||
|
Команды рассчитаны на Ubuntu 26.04 и Gitea 1.27.
|
||||||
|
|
||||||
|
## Зафиксированные параметры
|
||||||
|
|
||||||
|
- Gitea Runner: `2.3.0`, Linux amd64.
|
||||||
|
- SHA256: `1e9fb1bea022fdf40993ecbc1a13e87db1bfd3d7f42666e37f15f71d840d53b3`.
|
||||||
|
- Runner: repository-scoped, метка `de-roadmap-host:host`.
|
||||||
|
- Пользователь сервиса: `gitea-runner`, без `sudo` и Docker.
|
||||||
|
- Корень публикации: `/srv/de-roadmap`.
|
||||||
|
- Хранение: текущий релиз и две предыдущие версии.
|
||||||
|
- Окружение сборки: `/var/lib/gitea-runner/venvs/site`.
|
||||||
|
- Публичный IPv4 VPS: `167.224.64.252`.
|
||||||
|
|
||||||
|
## Предварительные условия
|
||||||
|
|
||||||
|
- Есть пользователь с `sudo` и рабочий SSH-доступ к VPS.
|
||||||
|
- Есть доступ к управлению DNS-зоной `dementev.space`.
|
||||||
|
- В репозитории `ddmitry/de-roadmap` включены Gitea Actions.
|
||||||
|
|
||||||
|
## Подготовка VPS
|
||||||
|
|
||||||
|
Установить Git, HTTP-сервер, Certbot, UFW и Python venv:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install \
|
||||||
|
ca-certificates \
|
||||||
|
certbot \
|
||||||
|
curl \
|
||||||
|
git \
|
||||||
|
jq \
|
||||||
|
nginx \
|
||||||
|
python3-certbot-nginx \
|
||||||
|
python3-venv \
|
||||||
|
ufw
|
||||||
|
```
|
||||||
|
|
||||||
|
Получить административный checkout, из которого устанавливаются tracked-файлы:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://git.dementev.space/ddmitry/de-roadmap.git
|
||||||
|
cd de-roadmap
|
||||||
|
```
|
||||||
|
|
||||||
|
Создать пользователя и каталоги:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo useradd \
|
||||||
|
--system \
|
||||||
|
--home-dir /var/lib/gitea-runner \
|
||||||
|
--create-home \
|
||||||
|
--shell /usr/sbin/nologin \
|
||||||
|
gitea-runner
|
||||||
|
sudo install -d -o gitea-runner -g gitea-runner -m 0755 \
|
||||||
|
/var/lib/gitea-runner/workspaces \
|
||||||
|
/srv/de-roadmap \
|
||||||
|
/srv/de-roadmap/releases
|
||||||
|
sudo install -d -o root -g root -m 0755 /etc/gitea-runner
|
||||||
|
```
|
||||||
|
|
||||||
|
Скачать runner во временный каталог, сверить checksum и установить root-owned
|
||||||
|
бинарник в `/usr/local/bin/gitea-runner`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
runner_tmp_dir=$(mktemp -d /tmp/de-roadmap-runner.XXXXXX)
|
||||||
|
curl -fsSLo "${runner_tmp_dir}/gitea-runner" \
|
||||||
|
https://dl.gitea.com/gitea-runner/2.3.0/gitea-runner-2.3.0-linux-amd64
|
||||||
|
printf '%s %s\n' \
|
||||||
|
'1e9fb1bea022fdf40993ecbc1a13e87db1bfd3d7f42666e37f15f71d840d53b3' \
|
||||||
|
"${runner_tmp_dir}/gitea-runner" \
|
||||||
|
| sha256sum --check
|
||||||
|
sudo install -o root -g root -m 0755 \
|
||||||
|
"${runner_tmp_dir}/gitea-runner" /usr/local/bin/gitea-runner
|
||||||
|
rm "${runner_tmp_dir}/gitea-runner"
|
||||||
|
rmdir "$runner_tmp_dir"
|
||||||
|
```
|
||||||
|
|
||||||
|
Из корня репозитория установить конфигурацию и unit:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo install -o root -g root -m 0644 \
|
||||||
|
project/ops/gitea-vps-site/gitea-runner.yaml \
|
||||||
|
/etc/gitea-runner/config.yaml
|
||||||
|
sudo install -o root -g root -m 0644 \
|
||||||
|
project/ops/gitea-vps-site/gitea-runner.service \
|
||||||
|
/etc/systemd/system/gitea-runner.service
|
||||||
|
sudo systemd-analyze verify /etc/systemd/system/gitea-runner.service
|
||||||
|
```
|
||||||
|
|
||||||
|
## Регистрация runner
|
||||||
|
|
||||||
|
Repository registration token получают в Gitea:
|
||||||
|
`ddmitry/de-roadmap` → Settings → Actions → Runners. Токен не сохраняют в Git
|
||||||
|
или shell history. Временный файл с токеном создают с владельцем
|
||||||
|
`gitea-runner:gitea-runner` и режимом `0600`. После регистрации файл
|
||||||
|
`/var/lib/gitea-runner/.runner` должен принадлежать тому же пользователю и
|
||||||
|
иметь режим `0600`.
|
||||||
|
|
||||||
|
Token-file должен содержать ровно 40 символов без завершающего перевода строки.
|
||||||
|
При извлечении JSON-ответа через `jq` использовать `jq --join-output '.token'`,
|
||||||
|
а не `jq --raw-output`, который добавляет newline.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo -u gitea-runner \
|
||||||
|
/usr/local/bin/gitea-runner \
|
||||||
|
--config /etc/gitea-runner/config.yaml \
|
||||||
|
register \
|
||||||
|
--no-interactive \
|
||||||
|
--instance https://git.dementev.space \
|
||||||
|
--token-file /var/lib/gitea-runner/.registration-token \
|
||||||
|
--name de-roadmap-vps
|
||||||
|
sudo rm /var/lib/gitea-runner/.registration-token
|
||||||
|
sudo chmod 0600 /var/lib/gitea-runner/.runner
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
sudo systemctl enable --now gitea-runner
|
||||||
|
systemctl is-enabled gitea-runner
|
||||||
|
systemctl is-active gitea-runner
|
||||||
|
```
|
||||||
|
|
||||||
|
Временный файл с токеном удаляют сразу после успешной регистрации. В Gitea на
|
||||||
|
странице Settings → Actions → Runners runner `de-roadmap-vps` должен перейти в
|
||||||
|
состояние online и показывать метку `de-roadmap-host`.
|
||||||
|
|
||||||
|
## Nginx и первичная публикация
|
||||||
|
|
||||||
|
Из корня репозитория установить bootstrap-конфигурацию nginx, создать ссылку,
|
||||||
|
отключить стандартный сайт Ubuntu и только затем перечитать проверенную
|
||||||
|
конфигурацию:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo install -o root -g root -m 0644 \
|
||||||
|
project/ops/gitea-vps-site/nginx.conf \
|
||||||
|
/etc/nginx/sites-available/de-roadmap
|
||||||
|
sudo ln -s \
|
||||||
|
/etc/nginx/sites-available/de-roadmap \
|
||||||
|
/etc/nginx/sites-enabled/de-roadmap
|
||||||
|
sudo unlink /etc/nginx/sites-enabled/default
|
||||||
|
sudo nginx -t
|
||||||
|
sudo systemctl reload nginx
|
||||||
|
```
|
||||||
|
|
||||||
|
Первый server block в `nginx.conf` возвращает `404` для неизвестных HTTP Host,
|
||||||
|
не раскрывает версию nginx и отклоняет TLS handshake для IP или неизвестного
|
||||||
|
SNI. Поэтому сертификат `de.dementev.space` не выдаётся при обращении к VPS по
|
||||||
|
IP. Если проверка конфигурации не прошла, отключить новый virtual host,
|
||||||
|
восстановить стандартный сайт и перечитать проверенную конфигурацию:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo unlink /etc/nginx/sites-enabled/de-roadmap
|
||||||
|
sudo ln -s \
|
||||||
|
/etc/nginx/sites-available/default \
|
||||||
|
/etc/nginx/sites-enabled/default
|
||||||
|
sudo nginx -t
|
||||||
|
sudo systemctl reload nginx
|
||||||
|
```
|
||||||
|
|
||||||
|
Файл `project/ops/gitea-vps-site/nginx.conf` предназначен только для запуска до
|
||||||
|
выпуска сертификата. После выпуска сертификата Certbot изменяет установленный
|
||||||
|
virtual host. Повторная установка bootstrap-файла поверх рабочего конфига
|
||||||
|
удалит TLS-директивы.
|
||||||
|
|
||||||
|
## Окружение сборки
|
||||||
|
|
||||||
|
Скрипт `.gitea/scripts/build-site.sh` создаёт persistent venv при первом запуске
|
||||||
|
и переиспользует его в следующих сборках. `pip install` выполняется каждый раз,
|
||||||
|
чтобы применить изменения `.gitea/requirements-site.txt`, но уже установленные
|
||||||
|
версии пакетов не переустанавливаются.
|
||||||
|
|
||||||
|
## Первый деплой
|
||||||
|
|
||||||
|
В Gitea открыть Actions → Deploy MkDocs to VPS, выбрать ветку `main` и нажать
|
||||||
|
Run workflow. Job `deploy` должен завершиться успешно. Проверить опубликованный
|
||||||
|
release и локальную выдачу nginx до переключения DNS:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
readlink -f /srv/de-roadmap/current
|
||||||
|
curl --fail --header 'Host: de.dementev.space' http://127.0.0.1/
|
||||||
|
```
|
||||||
|
|
||||||
|
Затем разрешить SSH, HTTP и HTTPS в UFW. Если SSH работает не на стандартном
|
||||||
|
порту `22`, сначала разрешить фактический порт вместо профиля `OpenSSH`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo ufw default deny incoming
|
||||||
|
sudo ufw default allow outgoing
|
||||||
|
sudo ufw allow OpenSSH
|
||||||
|
sudo ufw allow 'Nginx Full'
|
||||||
|
sudo ufw enable
|
||||||
|
sudo ufw status verbose
|
||||||
|
```
|
||||||
|
|
||||||
|
Ожидается политика `deny (incoming)` и разрешения только для SSH, `80/tcp` и
|
||||||
|
`443/tcp`.
|
||||||
|
|
||||||
|
## DNS и TLS
|
||||||
|
|
||||||
|
1. Уменьшить TTL записи `de.dementev.space`.
|
||||||
|
2. Направить `A` на VPS; удалить или корректно направить `AAAA`.
|
||||||
|
3. Убедиться, что сайт доступен извне по HTTP:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl --fail --head http://de.dementev.space/
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Выпустить сертификат и включить перенаправление HTTP на HTTPS:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo certbot --nginx \
|
||||||
|
--non-interactive \
|
||||||
|
--agree-tos \
|
||||||
|
--email me@dementev.space \
|
||||||
|
--redirect \
|
||||||
|
-d de.dementev.space
|
||||||
|
```
|
||||||
|
|
||||||
|
5. В созданном Certbot HTTP-блоке для `de.dementev.space` добавить
|
||||||
|
`server_tokens off;`, затем проверить и перечитать конфигурацию:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudoedit /etc/nginx/sites-available/de-roadmap
|
||||||
|
sudo nginx -t
|
||||||
|
sudo systemctl reload nginx
|
||||||
|
```
|
||||||
|
|
||||||
|
6. Проверить перенаправление, HTTPS, сертификат и автоматическое продление:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl --head http://de.dementev.space/
|
||||||
|
curl --fail --head https://de.dementev.space/
|
||||||
|
! curl --insecure --head https://167.224.64.252/
|
||||||
|
sudo certbot certificates
|
||||||
|
systemctl is-enabled certbot.timer
|
||||||
|
systemctl is-active certbot.timer
|
||||||
|
```
|
||||||
|
|
||||||
|
Ожидаются `301 Moved Permanently`, затем `200 OK`, отказ TLS по IP,
|
||||||
|
действующий сертификат и состояния таймера `enabled` и `active`.
|
||||||
|
|
||||||
|
7. Вернуть обычный DNS TTL.
|
||||||
|
|
||||||
|
## Проверка и откат
|
||||||
|
|
||||||
|
Активная версия определяется ссылкой `/srv/de-roadmap/current`. Для ручного
|
||||||
|
отката сначала выбрать точный release id из сохранённых каталогов, затем создать
|
||||||
|
временную ссылку и атомарно заменить `current`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
find /srv/de-roadmap/releases \
|
||||||
|
-mindepth 1 \
|
||||||
|
-maxdepth 1 \
|
||||||
|
-type d \
|
||||||
|
-printf '%f\n' \
|
||||||
|
| sort
|
||||||
|
|
||||||
|
rollback_release='<COMMIT_SHA>-<RUN_ID>'
|
||||||
|
rollback_link='/srv/de-roadmap/.current.rollback'
|
||||||
|
[[ "$rollback_release" =~ ^[0-9a-f]{40}-[0-9]+$ ]]
|
||||||
|
sudo test -d "/srv/de-roadmap/releases/${rollback_release}"
|
||||||
|
sudo test ! -e "$rollback_link"
|
||||||
|
sudo -u gitea-runner \
|
||||||
|
ln -s "releases/${rollback_release}" "$rollback_link"
|
||||||
|
sudo -u gitea-runner \
|
||||||
|
mv -Tf "$rollback_link" /srv/de-roadmap/current
|
||||||
|
readlink -f /srv/de-roadmap/current
|
||||||
|
curl --fail --head https://de.dementev.space/
|
||||||
|
```
|
||||||
|
|
||||||
|
Откат не удаляет более новые releases. Перед их ручным удалением всегда
|
||||||
|
проверять результат `readlink -f /srv/de-roadmap/current`.
|
||||||
|
|
||||||
|
Диагностика:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo systemctl status gitea-runner
|
||||||
|
sudo journalctl -u gitea-runner
|
||||||
|
sudo nginx -t
|
||||||
|
readlink -f /srv/de-roadmap/current
|
||||||
|
curl --fail --head https://de.dementev.space/
|
||||||
|
```
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Gitea Actions runner for de-roadmap
|
||||||
|
Documentation=https://docs.gitea.com/usage/actions
|
||||||
|
Wants=network-online.target
|
||||||
|
After=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=gitea-runner
|
||||||
|
Group=gitea-runner
|
||||||
|
WorkingDirectory=/var/lib/gitea-runner
|
||||||
|
ExecStart=/usr/local/bin/gitea-runner daemon --config /etc/gitea-runner/config.yaml
|
||||||
|
Restart=always
|
||||||
|
RestartSec=10s
|
||||||
|
TimeoutStopSec=45s
|
||||||
|
KillMode=mixed
|
||||||
|
|
||||||
|
Environment="PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
|
||||||
|
|
||||||
|
UMask=0022
|
||||||
|
NoNewPrivileges=true
|
||||||
|
PrivateDevices=true
|
||||||
|
PrivateTmp=true
|
||||||
|
ProtectClock=true
|
||||||
|
ProtectControlGroups=true
|
||||||
|
ProtectHome=true
|
||||||
|
ProtectHostname=true
|
||||||
|
ProtectKernelLogs=true
|
||||||
|
ProtectKernelModules=true
|
||||||
|
ProtectKernelTunables=true
|
||||||
|
ProtectSystem=strict
|
||||||
|
ReadWritePaths=/var/lib/gitea-runner /srv/de-roadmap
|
||||||
|
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
|
||||||
|
RestrictRealtime=true
|
||||||
|
RestrictSUIDSGID=true
|
||||||
|
LockPersonality=true
|
||||||
|
CapabilityBoundingSet=
|
||||||
|
SystemCallArchitectures=native
|
||||||
|
|
||||||
|
MemoryMax=1G
|
||||||
|
CPUQuota=100%
|
||||||
|
TasksMax=128
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
log:
|
||||||
|
level: info
|
||||||
|
|
||||||
|
runner:
|
||||||
|
file: /var/lib/gitea-runner/.runner
|
||||||
|
capacity: 1
|
||||||
|
timeout: 15m
|
||||||
|
shutdown_timeout: 30s
|
||||||
|
insecure: false
|
||||||
|
fetch_timeout: 5s
|
||||||
|
fetch_interval: 2s
|
||||||
|
fetch_interval_max: 10s
|
||||||
|
workdir_cleanup_age: 24h
|
||||||
|
idle_cleanup_interval: 10m
|
||||||
|
labels:
|
||||||
|
- "de-roadmap-host:host"
|
||||||
|
allocate_pty: false
|
||||||
|
|
||||||
|
cache:
|
||||||
|
enabled: false
|
||||||
|
|
||||||
|
container:
|
||||||
|
valid_volumes: []
|
||||||
|
docker_host: "-"
|
||||||
|
require_docker: false
|
||||||
|
|
||||||
|
host:
|
||||||
|
workdir_parent: /var/lib/gitea-runner/workspaces
|
||||||
|
|
||||||
|
health_check:
|
||||||
|
enabled: true
|
||||||
|
min_free_disk_space_mb: 1024
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
|
||||||
|
metrics:
|
||||||
|
enabled: false
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
server {
|
||||||
|
listen 80 default_server;
|
||||||
|
listen [::]:80 default_server;
|
||||||
|
listen 443 ssl default_server;
|
||||||
|
listen [::]:443 ssl default_server;
|
||||||
|
|
||||||
|
server_name _;
|
||||||
|
server_tokens off;
|
||||||
|
ssl_reject_handshake on;
|
||||||
|
|
||||||
|
return 404;
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
listen [::]:80;
|
||||||
|
|
||||||
|
server_name de.dementev.space;
|
||||||
|
root /srv/de-roadmap/current;
|
||||||
|
index index.html;
|
||||||
|
charset utf-8;
|
||||||
|
server_tokens off;
|
||||||
|
|
||||||
|
add_header X-Content-Type-Options "nosniff" always;
|
||||||
|
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~ /\. {
|
||||||
|
deny all;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
# Публикация сайта через Gitea Actions и VPS
|
||||||
|
|
||||||
|
Статус: реализовано 2026-08-05; механизм синхронизации GitHub-резерва требует
|
||||||
|
отдельного решения после восстановления доступа к GitHub. Инструкции по
|
||||||
|
восстановлению и эксплуатации находятся в
|
||||||
|
[`project/ops/gitea-vps-site/README.md`](../ops/gitea-vps-site/README.md).
|
||||||
|
|
||||||
|
## Проблема
|
||||||
|
|
||||||
|
Сайт `de.dementev.space` публиковался через GitHub Actions и GitHub Pages. После блокировки учётной записи GitHub основной домен стал недоступен, хотя исходный Markdown и конфигурация MkDocs сохранились, а основной Git-репозиторий уже размещён в собственной Gitea.
|
||||||
|
|
||||||
|
Публикация сайта не должна зависеть от доступности учётной записи GitHub. При этом рабочий процесс из исходной архитектуры сохраняется: изменение попадает в `main`, сайт собирается и обновляется автоматически без отдельного ручного деплоя.
|
||||||
|
|
||||||
|
## Цели
|
||||||
|
|
||||||
|
- Публиковать `de.dementev.space` с существующей VPS.
|
||||||
|
- Запускать сборку и публикацию автоматически после push в `main` в Gitea.
|
||||||
|
- Не заменять работающую версию сайта, если получение исходников или строгая сборка MkDocs завершились ошибкой.
|
||||||
|
- Ограничить права runner каталогами, необходимыми для сборки и публикации сайта.
|
||||||
|
- Сохранить GitHub Pages как заранее собираемый резерв на случай проблем с VPS.
|
||||||
|
- Сохранить минимальные накладные расходы и нулевые дополнительные расходы на хостинг.
|
||||||
|
|
||||||
|
## Не цели
|
||||||
|
|
||||||
|
- Автоматическое переключение между VPS и GitHub Pages.
|
||||||
|
- Жёсткие требования к времени восстановления или высокой доступности.
|
||||||
|
- Перенос еженедельной проверки внешних ссылок с GitHub Actions в Gitea Actions.
|
||||||
|
- Изменение существующих файлов в `.github/workflows/`.
|
||||||
|
- Добавление динамического приложения, авторизации или серверной базы данных.
|
||||||
|
|
||||||
|
## Исходное состояние на 2026-08-04
|
||||||
|
|
||||||
|
- MkDocs Material собирает статический каталог `site/`; `site_url` уже равен `https://de.dementev.space/`.
|
||||||
|
- Публичный репозиторий `ddmitry/de-roadmap` в Gitea является текущим `origin`.
|
||||||
|
- Gitea Actions для репозитория включены, но подходящего runner пока нет.
|
||||||
|
- Gitea видит существующие GitHub workflows, однако они зависят от GitHub Pages, GitHub-токенов и сторонних actions.
|
||||||
|
- На VPS, выбранной для сайта, пока нет HTTP-сервера; публично доступен только SSH.
|
||||||
|
|
||||||
|
## Выбранное решение
|
||||||
|
|
||||||
|
### Основной контур публикации
|
||||||
|
|
||||||
|
Gitea становится источником событий для основной публикации. На VPS с сайтом работает repository-scoped Gitea runner, зарегистрированный только для `de-roadmap`. Runner использует host mode и отдельную метку, предназначенную только для этого workflow.
|
||||||
|
|
||||||
|
Workflow хранится отдельно в `.gitea/workflows/deploy-site.yml`. Gitea выбирает `.gitea/workflows` раньше `.github/workflows`, поэтому GitHub-specific workflows сохраняются в репозитории, но не исполняются Gitea.
|
||||||
|
|
||||||
|
Workflow запускается после push в `main` и вручную. Он получает конкретный commit из публичного Gitea-репозитория обычным Git, создаёт изолированное Python-окружение, устанавливает закреплённые версии зависимостей и выполняет строгую сборку MkDocs. Сторонние `uses:` не применяются, поэтому сборка не зависит от GitHub Actions Marketplace.
|
||||||
|
|
||||||
|
### Изоляция runner
|
||||||
|
|
||||||
|
Runner работает как отдельный непривилегированный системный пользователь. У него нет `sudo`, членства в группе `docker` и доступа на запись к конфигурации nginx, сертификатам или другим сервисам VPS.
|
||||||
|
|
||||||
|
Пользователь runner может записывать только в собственный рабочий каталог и каталог релизов сайта. Workflow не запускается для pull request из недоверенных веток. Потребление памяти, CPU и количество процессов ограничиваются средствами менеджера сервисов операционной системы.
|
||||||
|
|
||||||
|
### Публикация и восстановление после ошибки
|
||||||
|
|
||||||
|
Каждая успешная сборка создаёт отдельную версию статического сайта. Новая версия становится активной только после завершения всех проверок. Переключение между версиями выполняется атомарно; частично собранный каталог никогда не становится корнем сайта.
|
||||||
|
|
||||||
|
Ошибка получения исходников, установки зависимостей или сборки оставляет активной предыдущую версию. Как минимум одна предыдущая успешная версия сохраняется для быстрого ручного отката.
|
||||||
|
|
||||||
|
### HTTP и HTTPS
|
||||||
|
|
||||||
|
Статические файлы обслуживает штатный nginx из репозитория Ubuntu. Nginx только читает активную версию сайта и не требует перезагрузки при обычной публикации контента.
|
||||||
|
|
||||||
|
TLS-сертификат для `de.dementev.space` получает и продлевает Certbot с интеграцией nginx. Перед переключением проверяются локальная сборка и конфигурация nginx, а DNS TTL заранее уменьшается. Затем DNS направляется на VPS, проверяется публичная доступность по HTTP и выпускается сертификат. Короткий интервал между переключением DNS и готовностью HTTPS допустим, поскольку жёсткого требования к непрерывной доступности нет.
|
||||||
|
|
||||||
|
### Резерв на GitHub Pages
|
||||||
|
|
||||||
|
После восстановления доступа к GitHub существующий GitHub workflow продолжает собирать сайт из GitHub-репозитория. Чтобы резерв оставался актуальным, изменения из основной Gitea должны автоматически синхронизироваться с GitHub. Предпочтительный кандидат — встроенный Gitea push mirror; окончательный механизм и его права будут согласованы отдельно после восстановления учётной записи. Резерв доступен по стандартному адресу GitHub Pages, но основной домен направлен на VPS.
|
||||||
|
|
||||||
|
GitHub Pages считается тёплым резервом контента и холодным резервом домена. При отказе VPS владелец вручную переключает DNS и при необходимости повторно активирует custom domain в настройках GitHub Pages. Допустимы задержка распространения DNS и ожидание выпуска TLS-сертификата; автоматический failover не требуется.
|
||||||
|
|
||||||
|
## Отклонённые варианты
|
||||||
|
|
||||||
|
- **Периодический pull с VPS:** проще инфраструктурно, но не даёт опыта работы с Gitea runner и менее наглядно связывает push с результатом сборки.
|
||||||
|
- **Runner рядом с Gitea:** усложняет доставку результата на VPS и позволяет сборкам влиять на ресурсы Git-сервера.
|
||||||
|
- **Jobs в Docker:** дают лучшую изоляцию, но требуют доступа runner к Docker и отдельного механизма передачи результата в каталог nginx. Для одного доверенного репозитория это лишняя сложность.
|
||||||
|
- **Caddy вместо nginx:** упрощает автоматический TLS, но штатный nginx лучше соответствует предпочтению владельца и доступен с обновлениями безопасности из основного репозитория Ubuntu. Certbot закрывает задачу TLS отдельно.
|
||||||
|
- **Cloudflare Pages, GitLab Pages или другой внешний Pages-сервис:** уменьшают нагрузку на VPS, но добавляют новую внешнюю учётную запись и зависимость, от которой как раз уходим.
|
||||||
|
- **Ожидание разблокировки GitHub:** сохраняет старую архитектуру, но оставляет сайт недоступным на неопределённый срок.
|
||||||
|
|
||||||
|
## Риски и меры
|
||||||
|
|
||||||
|
| Риск | Мера |
|
||||||
|
|------|------|
|
||||||
|
| Workflow исполняет команды непосредственно на VPS | Repository-scoped runner, отдельный пользователь без `sudo` и Docker, узкие права на запись, запуск только из `main`, системные лимиты ресурсов |
|
||||||
|
| Ошибка сборки ломает опубликованный сайт | Сборка в отдельной версии и атомарное переключение только после успеха |
|
||||||
|
| Сторонний action выполняет неожиданный код | Workflow состоит из собственных команд и не использует `uses:` |
|
||||||
|
| Gitea временно недоступна | Уже опубликованный сайт обслуживается независимо от Gitea |
|
||||||
|
| VPS недоступна или потеряна | Исходники остаются в Gitea; GitHub Pages служит ручным резервом после восстановления GitHub |
|
||||||
|
| После переключения DNS HTTPS ещё не готов | DNS TTL уменьшается заранее; Certbot запускается сразу после подтверждения публичного HTTP; короткий перерыв принят как допустимый |
|
||||||
|
| Сертификат GitHub Pages не готов во время аварии | Допускается задержка; на время восстановления используется стандартный адрес GitHub Pages |
|
||||||
|
| GitHub-резерв отстаёт от Gitea | После восстановления GitHub настраивается автоматическая синхронизация; до выбора механизма резерв не считается тёплым |
|
||||||
|
|
||||||
|
## Проверка реализации
|
||||||
|
|
||||||
|
- Runner после перезагрузки VPS автоматически подключается к Gitea и принимает только jobs с выделенной меткой.
|
||||||
|
- Push тестового изменения в `main` запускает ровно один Gitea workflow и публикует соответствующий commit.
|
||||||
|
- Gitea не запускает workflows из `.github/workflows/`, а сами файлы остаются без изменений.
|
||||||
|
- Workflow не обращается к GitHub за actions и не требует GitHub-токенов.
|
||||||
|
- Ошибка `mkdocs build --strict` завершает workflow с ошибкой и не изменяет публичную версию сайта.
|
||||||
|
- Успешная сборка переключает сайт целиком, без периода частично обновлённого содержимого.
|
||||||
|
- Пользователь runner не может использовать `sudo`, Docker или изменять конфигурацию nginx.
|
||||||
|
- `https://de.dementev.space/` отдаёт собранный сайт с действующим сертификатом после переключения DNS.
|
||||||
|
- Nginx и runner восстанавливаются после перезагрузки VPS без ручного запуска.
|
||||||
|
- После восстановления GitHub и настройки синхронизации резервная сборка получает тот же commit и остаётся доступна по стандартному адресу GitHub Pages.
|
||||||
|
|
||||||
|
## Влияние на документацию
|
||||||
|
|
||||||
|
Эта спецификация заменяет решения о публикации и custom domain из `project/ADR.md`. Решения того документа о MkDocs Material, структуре файлов и dual-compatible links остаются актуальными.
|
||||||
|
|
||||||
|
После реализации нужно обновить `AGENTS.md`, `project/PRD.md` и `project/TODO.md`, чтобы они описывали фактический основной контур публикации. Ссылку на исходный репозиторий в `mkdocs.yml` следует направить на Gitea; ссылку на GitHub можно сохранить как дополнительную после восстановления доступа.
|
||||||
|
|
||||||
|
## Открытый вопрос
|
||||||
|
|
||||||
|
После восстановления GitHub нужно окончательно выбрать способ автоматической синхронизации резервного репозитория. Базовый кандидат — Gitea push mirror с синхронизацией при каждом push; GitHub при этом становится read-only зеркалом, поскольку mirror перезаписывает расходящиеся изменения.
|
||||||
Reference in New Issue
Block a user