Compare commits
3 Commits
7d711425fd
...
a3e53b21e6
| Author | SHA1 | Date | |
|---|---|---|---|
|
a3e53b21e6
|
|||
|
1b120e3ae6
|
|||
|
a22be7c7d1
|
@@ -8,6 +8,133 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Шаг 9 — раскатана база и приложения без запуска (2026-05-23, выполнено)
|
||||||
|
|
||||||
|
На свежей Timeweb-машине прогнаны два плейбука без даунтайма источника
|
||||||
|
(контейнеры на target не запускались).
|
||||||
|
|
||||||
|
### 9a. Системная база
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run ansible -i timeweb.yml -m ping server # pong
|
||||||
|
uv run ansible-playbook -i timeweb.yml --diff playbook-all-setup.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
После прогона на target поднято: apt-пакеты (`geerlingguy.security`),
|
||||||
|
docker + сети (`web_proxy_network`, `monitoring_network`), eget с
|
||||||
|
инструментами (restic, rclone, btop, zellij и др.), ufw (порты 22,
|
||||||
|
2222, 80, 443), fail2ban, backup-инфра (`backup-all.py`,
|
||||||
|
resticprofile, cron).
|
||||||
|
|
||||||
|
Заодно `geerlingguy.security` отключил root по SSH и
|
||||||
|
`PasswordAuthentication` — root-канал закрыт, доступ только через
|
||||||
|
`major` + ключ. Перепроверено `ssh major@<новый-ip>` — работает.
|
||||||
|
|
||||||
|
### 9b. Application-плейбуки без запуска контейнеров
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run ansible-playbook -i timeweb.yml --diff \
|
||||||
|
--skip-tags run-app \
|
||||||
|
playbook-all-applications.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
На target созданы все `<app>`-пользователи с правильными uid/gid
|
||||||
|
(совпадают с источником, см. таблицу в [плане](timeweb.md)), каталоги
|
||||||
|
`/srv/applications/<app>/{data,config,backups}`, отрендерены
|
||||||
|
`docker-compose.yml` и application-конфиги. Контейнеры **не**
|
||||||
|
запускались — это шаг 5 cutover'а (после rsync'а данных).
|
||||||
|
|
||||||
|
OAuth-аутентификация в `cr.yandex` (из Шага 3) сработала с
|
||||||
|
Timeweb-айпишника без замечаний — `community.docker.docker_login` в
|
||||||
|
плейбуках homepage и transcriber прошёл.
|
||||||
|
|
||||||
|
### Обнаруженный латентный bug ordering'а в goaccess
|
||||||
|
|
||||||
|
На fresh-install упала задача
|
||||||
|
`playbook-goaccess.yml:55 «Ensure caddy access log exists before
|
||||||
|
goaccess starts»` — пыталась туч'ить файл в `/var/log/caddy/`, который
|
||||||
|
к этому моменту не существовал. Причина: каталог создаётся в
|
||||||
|
`playbook-caddyproxy.yml`, а в `playbook-all-applications.yml`
|
||||||
|
goaccess идёт **раньше** caddyproxy (caddyproxy специально последний,
|
||||||
|
чтобы стартовать после backends). На предыдущем сервере не проявлялось
|
||||||
|
— каталог уже существовал от прошлых прогонов.
|
||||||
|
|
||||||
|
Фикс: добавил в `playbook-goaccess.yml` явное создание
|
||||||
|
`caddy_logs_dir` перед touch'ем `access.log`. Owner/mode выставит
|
||||||
|
caddyproxy при своём прогоне, идемпотентность сохранена.
|
||||||
|
|
||||||
|
**Backlog (после миграции):** `caddy_logs_dir` — shared-ресурс между
|
||||||
|
плеями (caddyproxy пишет, goaccess читает), концептуально это
|
||||||
|
provisioning-time забота. Вынести его создание в `playbook-system.yml`
|
||||||
|
(или в отдельный shared-resources плей в `playbook-all-setup.yml`) и
|
||||||
|
убрать дубль из goaccess/caddyproxy. Делать после переезда отдельным
|
||||||
|
PR, не во время миграции.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Шаг 8 — VPS заказан, пользователь `major` создан (2026-05-23, выполнено)
|
||||||
|
|
||||||
|
Заказан Cloud VPS в Timeweb по тарифу из плана (4 × 3.3 ГГц, 8 ГБ RAM,
|
||||||
|
80 ГБ NVMe, Ubuntu 24.04 LTS), ДЦ Санкт-Петербург.
|
||||||
|
|
||||||
|
Первая выданная VPS попала на гипервизор с битой сетью: TCP-handshake
|
||||||
|
проходил нормально, но первый data-сегмент в любой TCP-сессии не
|
||||||
|
доставлялся ни в одну сторону. Подтверждено:
|
||||||
|
|
||||||
|
- `nc -l 12345` на сервере не получал данные от клиента, при этом
|
||||||
|
клиент видел `Connection succeeded`;
|
||||||
|
- strace зависшего `sshd: [accepted]`-child показывал
|
||||||
|
`read(socket, ..., 1) = ERESTARTSYS`, далее `SIGALRM` через 120 сек
|
||||||
|
по `LoginGraceTime` → exit (т.е. sshd ушёл в `read()` за клиентским
|
||||||
|
баннером и не дождался);
|
||||||
|
- `iptables -S` / `nft list ruleset` / `ufw status` — пусто, локального
|
||||||
|
firewall нет;
|
||||||
|
- исходящие соединения с VM (`curl http://example.com`) работали
|
||||||
|
штатно — ломались только входящие data-сегменты после handshake.
|
||||||
|
|
||||||
|
Ребут и переустановка ОС из панели не помогли. Пересоздал VPS в ДЦ СПб
|
||||||
|
с новым IP — заработало с первой попытки. Потеря времени ~1 час; на
|
||||||
|
будущее: при таком паттерне сразу пересоздаём в другом ДЦ, глубже
|
||||||
|
диагностику не ведём (это однозначно проблема сети провайдера).
|
||||||
|
|
||||||
|
### Bootstrap пользователя `major`
|
||||||
|
|
||||||
|
На свежей VPS только root по SSH-ключу. Поднял пользователя
|
||||||
|
аналогично YC-серверу — sudo через NOPASSWD, вход только по ключу.
|
||||||
|
Дальше `geerlingguy.security` + `roles/owner` пересоздадут пользователя
|
||||||
|
идемпотентно с теми же uid/gid и приклеят политику sshd при первом
|
||||||
|
прогоне ансибла.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Создать пользователя с home и bash, добавить в sudo
|
||||||
|
useradd -m -s /bin/bash major
|
||||||
|
usermod -aG sudo major
|
||||||
|
|
||||||
|
# 2. NOPASSWD-политика sudo
|
||||||
|
echo 'major ALL=(ALL) NOPASSWD: ALL' > /etc/sudoers.d/major
|
||||||
|
chmod 0440 /etc/sudoers.d/major
|
||||||
|
visudo -cf /etc/sudoers.d/major # должно сказать "parsed OK"
|
||||||
|
|
||||||
|
# 3. SSH-ключ (тот же, что залит для root при создании VPS)
|
||||||
|
install -d -m 700 -o major -g major /home/major/.ssh
|
||||||
|
install -m 600 -o major -g major \
|
||||||
|
/root/.ssh/authorized_keys \
|
||||||
|
/home/major/.ssh/authorized_keys
|
||||||
|
```
|
||||||
|
|
||||||
|
Проверка с локальной машины:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh major@<новый-ip>
|
||||||
|
sudo whoami # root, без пароля
|
||||||
|
```
|
||||||
|
|
||||||
|
Прошло. Root-доступ по SSH пока оставлен как резервный канал — первый
|
||||||
|
прогон ансибла отключит его через `geerlingguy.security`
|
||||||
|
(`PermitRootLogin no`, `PasswordAuthentication no`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Шаг 7 — `run-app` тег + унификация registry (2026-05-22, выполнено)
|
## Шаг 7 — `run-app` тег + унификация registry (2026-05-22, выполнено)
|
||||||
|
|
||||||
По итогам аудита подготовительных задач выявлены и закрыты две
|
По итогам аудита подготовительных задач выявлены и закрыты две
|
||||||
|
|||||||
@@ -1,63 +1,63 @@
|
|||||||
---
|
---
|
||||||
- name: 'Configure netdata'
|
- name: "Configure netdata"
|
||||||
ansible.builtin.import_playbook: playbook-netdata.yml
|
ansible.builtin.import_playbook: playbook-netdata.yml
|
||||||
|
|
||||||
#
|
#
|
||||||
|
|
||||||
- name: 'Configure dozzle'
|
- name: "Configure dozzle"
|
||||||
ansible.builtin.import_playbook: playbook-dozzle.yml
|
ansible.builtin.import_playbook: playbook-dozzle.yml
|
||||||
|
|
||||||
- name: 'Configure goaccess'
|
- name: "Configure gitea"
|
||||||
ansible.builtin.import_playbook: playbook-goaccess.yml
|
|
||||||
|
|
||||||
- name: 'Configure gitea'
|
|
||||||
ansible.builtin.import_playbook: playbook-gitea.yml
|
ansible.builtin.import_playbook: playbook-gitea.yml
|
||||||
|
|
||||||
- name: 'Configure gramps'
|
- name: "Configure gramps"
|
||||||
ansible.builtin.import_playbook: playbook-gramps.yml
|
ansible.builtin.import_playbook: playbook-gramps.yml
|
||||||
|
|
||||||
- name: 'Configure memos'
|
- name: "Configure memos"
|
||||||
ansible.builtin.import_playbook: playbook-memos.yml
|
ansible.builtin.import_playbook: playbook-memos.yml
|
||||||
|
|
||||||
- name: 'Configure miniflux'
|
- name: "Configure miniflux"
|
||||||
ansible.builtin.import_playbook: playbook-miniflux.yml
|
ansible.builtin.import_playbook: playbook-miniflux.yml
|
||||||
|
|
||||||
- name: 'Configure outline'
|
- name: "Configure outline"
|
||||||
ansible.builtin.import_playbook: playbook-outline.yml
|
ansible.builtin.import_playbook: playbook-outline.yml
|
||||||
|
|
||||||
- name: 'Configure rssbridge'
|
- name: "Configure rssbridge"
|
||||||
ansible.builtin.import_playbook: playbook-rssbridge.yml
|
ansible.builtin.import_playbook: playbook-rssbridge.yml
|
||||||
|
|
||||||
- name: 'Configure wakapi'
|
- name: "Configure wakapi"
|
||||||
ansible.builtin.import_playbook: playbook-wakapi.yml
|
ansible.builtin.import_playbook: playbook-wakapi.yml
|
||||||
|
|
||||||
- name: 'Configure wanderer'
|
- name: "Configure wanderer"
|
||||||
ansible.builtin.import_playbook: playbook-wanderer.yml
|
ansible.builtin.import_playbook: playbook-wanderer.yml
|
||||||
|
|
||||||
- name: 'Configure calibre'
|
- name: "Configure calibre"
|
||||||
ansible.builtin.import_playbook: playbook-calibre.yml
|
ansible.builtin.import_playbook: playbook-calibre.yml
|
||||||
|
|
||||||
- name: 'Configure remembos'
|
- name: "Configure remembos"
|
||||||
ansible.builtin.import_playbook: playbook-remembos.yml
|
ansible.builtin.import_playbook: playbook-remembos.yml
|
||||||
|
|
||||||
- name: 'Configure apprise'
|
- name: "Configure apprise"
|
||||||
ansible.builtin.import_playbook: playbook-apprise.yml
|
ansible.builtin.import_playbook: playbook-apprise.yml
|
||||||
|
|
||||||
- name: 'Configure tuwunel'
|
- name: "Configure tuwunel"
|
||||||
ansible.builtin.import_playbook: playbook-tuwunel.yml
|
ansible.builtin.import_playbook: playbook-tuwunel.yml
|
||||||
|
|
||||||
#
|
#
|
||||||
|
|
||||||
- name: 'Configure homepage'
|
- name: "Configure homepage"
|
||||||
ansible.builtin.import_playbook: playbook-homepage.yml
|
ansible.builtin.import_playbook: playbook-homepage.yml
|
||||||
|
|
||||||
- name: 'Configure transcriber'
|
- name: "Configure transcriber"
|
||||||
ansible.builtin.import_playbook: playbook-transcriber.yml
|
ansible.builtin.import_playbook: playbook-transcriber.yml
|
||||||
|
|
||||||
#
|
#
|
||||||
|
|
||||||
- name: 'Configure authelia'
|
- name: "Configure authelia"
|
||||||
ansible.builtin.import_playbook: playbook-authelia.yml
|
ansible.builtin.import_playbook: playbook-authelia.yml
|
||||||
|
|
||||||
- name: 'Configure caddy proxy'
|
- name: "Configure caddy proxy"
|
||||||
ansible.builtin.import_playbook: playbook-caddyproxy.yml
|
ansible.builtin.import_playbook: playbook-caddyproxy.yml
|
||||||
|
|
||||||
|
- name: "Configure goaccess"
|
||||||
|
ansible.builtin.import_playbook: playbook-goaccess.yml
|
||||||
|
|||||||
@@ -52,6 +52,13 @@
|
|||||||
- "{{ db_dir }}"
|
- "{{ db_dir }}"
|
||||||
- "{{ report_dir }}"
|
- "{{ report_dir }}"
|
||||||
|
|
||||||
|
# Owner/mode проставит caddyproxy при своём (позднем) прогоне.
|
||||||
|
- name: "Ensure caddy logs directory exists"
|
||||||
|
ansible.builtin.file:
|
||||||
|
path: "{{ caddy_logs_dir }}"
|
||||||
|
state: "directory"
|
||||||
|
mode: "0755"
|
||||||
|
|
||||||
- name: "Ensure caddy access log exists before goaccess starts"
|
- name: "Ensure caddy access log exists before goaccess starts"
|
||||||
ansible.builtin.copy:
|
ansible.builtin.copy:
|
||||||
content: ""
|
content: ""
|
||||||
@@ -77,8 +84,8 @@
|
|||||||
group: "{{ app_user }}"
|
group: "{{ app_user }}"
|
||||||
mode: "{{ item.mode }}"
|
mode: "{{ item.mode }}"
|
||||||
loop:
|
loop:
|
||||||
- {name: "Dockerfile", mode: "0640"}
|
- { name: "Dockerfile", mode: "0640" }
|
||||||
- {name: "entrypoint.sh", mode: "0750"}
|
- { name: "entrypoint.sh", mode: "0750" }
|
||||||
|
|
||||||
- name: "Run application with docker compose"
|
- name: "Run application with docker compose"
|
||||||
community.docker.docker_compose_v2:
|
community.docker.docker_compose_v2:
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
ungrouped:
|
||||||
|
hosts:
|
||||||
|
server:
|
||||||
|
ansible_host: "92.53.105.41"
|
||||||
|
ansible_user: "major"
|
||||||
|
ansible_become: true
|
||||||
Reference in New Issue
Block a user