Files
admin/.wiki/sources/iis-host-migration-2026-05-19.md
vitya 98bcc37d32 import: .wiki/sources/ from MoreThenCms via subtree-split
git-subtree-dir: .wiki/sources
git-subtree-mainline: d25c0577c8
git-subtree-split: a5e96432bc
2026-05-21 13:46:39 +03:00

27 KiB
Raw Permalink Blame History

title, type, tags, ingested, raw_path, updated
title type tags ingested raw_path updated
IIS Host Migration Session 2026-05-19 source
migration
iis
windows
recovery
traefik
2026-05-19 ../../.tasks/iis-on-host-migration.md 2026-05-21

IIS Host Migration Session 2026-05-19

Перенос основного CMS-сайта (MoreThenCms.Web, обслуживает 11 клиентских доменов через Host header) с IIS внутри snolla-recovery-vm на нативный IIS windows-recovery-host. Цель — убрать VBox как слой нестабильности.

Хронология

Старт сессии (после nas-recovery-session-2026-05-18) — VM один раз отвалилась сетью, оживили через VBoxManage guestcontrol + ipconfig /release /renew (recipe в vbox-windows-stability-tuning подтверждён ещё раз).

Phase 1 — discovery (read-only):

  • В VM через SSH (NAT-forward 127.0.0.1:8022, не bridged 192.168.1.15 — это устарело): appcmd list site/apppool/app/vdir /xml.
  • На хосте: Get-WindowsOptionalFeature -Online IIS-* через elevated PS — все нужные фичи (IIS-WebServer, IIS-ASPNET45, IIS-NetFxExtensibility45, IIS-ISAPIFilter, IIS-ManagementConsole, IIS-IIS6ManagementCompatibility, IIS-Metabase) уже установлены. WebAdministration PS-module грузится из admin-PS.
  • В источниках на хосте (grep): hardcoded C:\inetpub\wwwroot\ в коде CMS нет — всё через ConfigurationManager.AppSettings["sitePath"]. НО в prod Web.config (из backup vm-sites\) ключ sitePath явно проставлен абсолютным путём — патч в Phase 2 необходим.

Phase 1 находки (важные расхождения с wiki):

  • Snolla.IdentityManager в VM на :8089, не :80 (вики говорила :80). Sub-apps stostayer.old /calc, /price, /price/tireService существуют с отдельными app pools.
  • C:\stayer\Snolla.IdentityManager\Web.config указывает на SRV-1135520-1\SQLEXPRESS + Integrated Security — мёртвый внешний сервер. Сайт точно не работает, deploy-артефакт без живого консьюмера.
  • C:\stayer\MoreThenCms.Web\Web.config (stostayer) использует внешний production MSSQL 89.253.219.2,1433 с user stostayer — это другая инфраструктура, не наш контейнер.

Phase 2 — миграция (admin PS):

  • Default Web Site остановлен (autoStart=false).
  • robocopy из C:\nas-recovery\vm-sites\C:\sites\MoreThenCms.Web (8.7 GB / 44.7k файлов / 1m13s) и → C:\stayer\ (2.2 GB / 13.9k файлов / 21s).
  • Web.config patch только для C:\sites\MoreThenCms.Web\Web.config: sitePath C:\inetpub\wwwroot\MoreThenCms.Web\C:\sites\MoreThenCms.Web\ + Data Source=10.0.2.2Data Source=localhost. UTF-8 with BOM через [System.IO.File]::WriteAllText (паттерн из cms-config-rewrite-pattern).
  • 3 AppPool (.NET v4.0 Integrated, ApplicationPoolIdentity): MoreThenCms.Web, stostayer, stostayer.old.
  • 3 IIS-сайта: MoreThenCms.Web *:80 (catch-all), stostayer *:8090, stostayer.old *:8091.
  • ACL icacls /grant 'IIS AppPool\<site>:(OI)(CI)M' /T на каждый physical root.
  • Skip: Snolla.IdentityManager (по решению — публично не нужен), 3 sub-apps stostayer.old (по решению).
  • Smoke test через http://localhost/ с Host header: все 11 хостов отвечают (большинство 302 redirect — CMS работает). rimiz.ru — 404 (CMS-side, не инфра). stostayer/oldstostayer на :8090/:8091 — timeout, в текущей сессии не разбирались.

Phase 3 — traefik switch (file-provider auto-reload):

  • Backup *.yml.bak-phase3-2026-05-19 для 11 файлов в C:\Users\vitya\projects\docker\diskstation\traefik\data\custom\.
  • Patch: host.docker.internal:18080host.docker.internal:80 (11 yml: snolla, rimiz, labtools, labtoolspro, pilorama98, tandemmebel, emspb, kupimknigi, maljarka, sestech, isc-artmaterials). UTF-8 без BOM.
  • Не трогали stostayer.yml (:18180) и oldstostayer.yml (:18181) — stayer-сайты остаются на VM.
  • Public smoke через https://localhost:4443/ с Host header: 10/11 → HTTP 200. Только rimiz.ru → 404 (как и в локальном тесте).

Результат

Production-трафик 10 главных доменов теперь идёт полностью без VM: client → OpenWRT → traefik → host-IIS native → MSSQL container на хосте. VM продолжает обслуживать только stostayer/oldstostayer через старые NAT port forwards (:18180/:18181).

Открытые вопросы / нюансы

  • rimiz.ru отдаёт 404 на host-IIS (и на VM до миграции тоже отдавал?). CMS-routing не знает этот host. Проверить mapping в БД / CMS-сайт-таблице.
  • stostayer + stostayer.old timeout на host-IIS (http://localhost:8090/, :8091). Возможно cold-start ASP.NET, возможно реальный bug. Сейчас не разбирали — traefik для них всё ещё на VM.
  • Snolla.IdentityManager не мигрирован — если что-то внутри CMS дёргает его через localhost:8089, может тихо ломаться (пока симптомов не видно).
  • VM можно глушить только после: либо доделать stostayer на host, либо явное решение не мигрировать stayer на этот хост.
  • Phase 5 не сделан: URL Rewrite + ARR (фикс :4443 в редиректах CMS), Application Initialization (warm-start), log rotation C:\inetpub\logs\.

Технические артефакты

  • .tasks/iis-on-host-migration.md — детальный план фаз, обновлён со статусом Phase 1-3 = done.
  • Backup конфигов traefik: *.yml.bak-phase3-2026-05-19.
  • Скрипты Phase 2 и Phase 3 — в истории чата сессии (не сохранены отдельным файлом, идемпотентны на повторный запуск).

Phase 4 — реорг C:\sites\ + перенос stayer'ов (вечер 2026-05-19)

Пользователь возмутился разбросом (C:\sites\ только для snolla, C:\stayer\ для stayer). Реорганизация ради единой иерархии:

  • C:\sites\MoreThenCms.WebC:\sites\snolla (rename)
  • C:\stayer\MoreThenCms.WebC:\sites\stostayer (move)
  • C:\stayer\stostayer.oldC:\sites\stostayer.old (move)
  • C:\stayer\Snolla.IdentityManagerC:\sites\snolla-identity-manager (move + kebab-rename)
  • Удалены: C:\stayer\Mis.StoStayer.{Price,TireService,Calculator}.{Api,Web} (sub-apps не нужны).
  • C:\stayer\ полностью удалён.

IIS sync:

  • Site MoreThenCms.Web → переименован в snolla (Set-ItemProperty IIS:\Sites\... -Name name)
  • AppPool MoreThenCms.Webrename невозможен in-place, пересоздан как snolla с тем же набором properties (.NET v4.0 Integrated, ApplicationPoolIdentity), site rebound, старый pool удалён.
  • physicalPath всех 3 sites обновлён под C:\sites\<name>.
  • Web.config sitePath patches:
    • C:\sites\snolla\Web.config: C:\sites\MoreThenCms.Web\C:\sites\snolla\
    • C:\sites\stostayer\Web.config: C:\stayer\MoreThenCms.Web\C:\sites\stostayer\
  • ACL re-grant + cleanup stale ACE для IIS AppPool\MoreThenCms.Web.

Phase 5 — stostayer.old DB-conn патч (запутанная история conn-string'ов в stayer)

Локальный smoke :8091 падал в timeout. Причина: C:\sites\stostayer.old\web.config имел Data Source=10.0.2.2 (адрес VBox NAT gateway, на хосте не резолвится) с user=snolla — фактически идентичная нашему MSSQL контейнеру конфигурация, только адрес неправильный. Patch: 10.0.2.2localhost. После — :8091 → 200.

Это наша ошибка Phase 2 — пропустили patch этого файла, так как фокус был только на MoreThenCms.Web\Web.config.

Phase 6 — stostayer DB-conn миграция на новый сервер (www.stostayer.ru)

Локальный smoke :8090 тоже timeout — но по другой причине. C:\sites\stostayer\Web.config указывал на 89.253.219.2,1433 (внешний production MSSQL), оказался полностью недоступен: TCP timeout, ping fail, traceroute затухает на 8-м hop у 139.45.230.171. На VM (через тот же путь) тоже не работал — пользователь подтвердил.

Пользователь предоставил новые creds: www.stostayer.ru,1433 / stayer_site / ^I9D)LB)DK)8J#xBDG$t}W&_ioaT!M!LF. Тест из не-elevated PS: TCP reachable, SQL login OK (SQL Server 2022). Patched Web.config.

Pitfall: после patch IIS отдавал HTTP 500. Причина — символ & в пароле, который в XML является зарезервированным. ASP.NET Web.config XML парсер падал на парсинг conn-string. Fix: &&amp; (XML entity escape). См. webconfig-password-xml-escape.

Phase 7 — отключение traefik routes для stayer'ов

Пользователь решил: stayer'ы наружу светить не нужно вообще (даже после миграции на хост). stostayer.yml и oldstostayer.yml переименованы в *.yml.disabled — traefik file-provider не подхватывает, route gone. Site'ы на хосте :8090/:8091 остаются для возможного внутреннего использования.

Backup yml для этих изменений: *.yml.bak-stayer-switch-2026-05-19 (содержит вариант с переключением на :8090/:8091 — на случай если решим включить обратно).

Phase 8 — VM savestate

После того как 100% prod-трафика идёт через host-IIS, VM snolla-recovery заморожена через VBoxManage controlvm "snolla-recovery" savestate. VMState = saved. Конфигурация и диски сохранены — resume за 5-10 сек если что-то понадобится. Не удалена.

Финальное состояние (2026-05-19 конец сессии)

C:\sites\
  ├── snolla\                   (was MoreThenCms.Web in C:\inetpub\wwwroot, was in vm-sites\wwwroot\MoreThenCms.Web)
  ├── stostayer\                (was C:\stayer\MoreThenCms.Web)
  ├── stostayer.old\            (was C:\stayer\stostayer.old)
  └── snolla-identity-manager\  (was C:\stayer\Snolla.IdentityManager — deploy artifact, без живого consumer'а)

IIS sites/pools (имя=имя=пуло):
  snolla         *:80       → C:\sites\snolla         (pool snolla, .NET v4.0 Integrated, AppPoolIdentity)
  stostayer      *:8090     → C:\sites\stostayer      (pool stostayer)
  stostayer.old  *:8091     → C:\sites\stostayer.old  (pool stostayer.old)

Connection strings:
  snolla:         Data Source=localhost      → MSSQL container на хосте (DB MoreThenCms)
  stostayer:      Data Source=www.stostayer.ru,1433 (user stayer_site)   → внешний SQL Server 2022
  stostayer.old:  Data Source=localhost      → MSSQL container на хосте (DB stostayer, user snolla)

Traefik active routes:  10 главных доменов (snolla.com и др.) + rimiz.ru → host:80.
                        Stayer routes (stostayer.snolla.com, old.stostayer.ru) DISABLED — приватны.
                        Mёртвые: disk.yml, dsm.yml (192.168.1.10).

VM snolla-recovery: VMState=saved (frozen, ~92 GB на диске + ~3 GB savestate RAM dump).

Открытые вопросы (что унесли в следующую сессию)

  • rimiz.ru отдаёт 404 на host-IIS. CMS-routing не знает этот host — нужно копнуть БД (таблица CMS-сайтов, mapping host header → site).
  • Snolla.IdentityManager на хосте: папка перенесена, но IIS-сайт не создавался (по решению). Если CMS-код где-то дёргает localhost:8089 или подобное — будет тихий fail. Симптомов пока нет.
  • Phase 5 (из старого плана): URL Rewrite + ARR для фикса :4443 в редиректах, Application Initialization (warm-start), log rotation C:\inetpub\logs\. Не блокеры.
  • VM-cleanup: если через ~неделю стабильной работы host'а проблем не будет — VBoxManage unregistervm "snolla-recovery" --delete освободит ~92 GB. NAT port forwards в OpenWRT (host:18080/18180/18181/18189) можно удалить.

Связано

recovery-architecture-snapshot обновлён под полностью-host chain. snolla-recovery-vm — VMState=saved, все sites unused. windows-recovery-host — финальный layout C:\sites\. webconfig-password-xml-escape — новый concept про XML entity escape в conn-string'ах. cms-config-rewrite-pattern — UTF-8 BOM подтверждён ещё много раз.


Phase 9 — RЕVERT (вечер 2026-05-19, после провала)

Что случилось: через ~10 минут после моего commit'a "done" пользователь открыл https://www.pilorama98.ru/502 Bad Gateway. Дальше остальные домены показали TOO_MANY_REDIRECTS.

Root cause (расписан полностью в iis-migration-2026-05-19-postmortem):

  • Phase 3 я patched traefik backend host.docker.internal:18080host.docker.internal:80.
  • Внутри traefik-контейнера host.docker.internal:80 через Docker Desktop NAT резолвится обратно в сам traefik на его HTTP entrypoint :80 (potential Docker publish-port loopback gotcha).
  • traefik https.yml имеет http-catchall middleware redirect-to-https → traefik отвечает 301 на свой же запрос → loop.

Реактивная цепочка ошибок (~1 час):

  • Restart-WebAppPool, nuke workers, docker restart traefik → only sделали хуже (503).
  • Patched host.docker.internal:80 → 192.168.1.143:80 → тот же loop (LAN-IP через NAT тоже возвращается в traefik).
  • Patched на :8088 + добавил IIS binding → нужен Stop+Start Website чтобы binding applied, изначально забыл → IIS не listen → 503.
  • Думал что 301 от CMS (Pitfall 5 X-Forwarded-Proto), копал CMS код — реальный источник был самим traefik (headers без Server: Microsoft-IIS подсказывали).

Revert (выполнен):

  • VBoxManage startvm "snolla-recovery" — VM поднята из savestate (~10s + 90s warmup + recipe ipconfig /release /renew для застрявшего network adapter из vbox-windows-stability-tuning).
  • 11 главных yml восстановлены из *.bak-phase3-2026-05-19 (host.docker.internal:18080 → VM).
  • Stayer yml: stostayer.yml.disabled / oldstostayer.yml.disabled удалены, восстановлены из *.bak-stayer-switch-2026-05-19 (:18180/18181 → VM).
  • docker restart traefik (file-provider не подхватил reload автоматом).
  • Public smoke через VM-chain → 7/11 хостов отвечают (2 c 200, 5 с CMS-side редиректами на canonical — нормально для CMS-логики); проверено пользователем в браузере → работает.

Финальное состояние (после revert)

Production снова на VM-chain (как было в начале сессии 2026-05-19). На хосте осталось:

  • C:\sites\{snolla, stostayer, stostayer.old, snolla-identity-manager} — ~11 GB, inert (не на prod-пути)
  • IIS sites/pools snolla, stostayer, stostayer.old — не получают трафика (traefik backend назад на VM)
  • traefik backups: *.bak-phase3-2026-05-19, *.bak-stayer-switch-2026-05-19, *.bak-hostip-2026-05-19
  • VM snolla-recovery — running, IIS активен, обслуживает 11 главных доменов + 2 stayer публично через port forwards.

Артефакты для следующей попытки (НЕ удалять):

  • Web.config'и в C:\sites\ пропатчены правильно (sitePath, conn-strings, stostayer на новый DB) — можно переиспользовать.
  • IIS sites/pools уже настроены — переключение требует только traefik backend patch + tested correctly.
  • Recipe для правильной миграции — в iis-migration-2026-05-19-postmortem.

Phase 10 — Attempt 2 (вечер 2026-05-19, после прочтения post-mortem)

Повторная попытка миграции, выполненная по recipe из iis-migration-2026-05-19-postmortem. Все 5 главных правил соблюдены, миграция прошла без incidents.

Что сделано

  1. Read-only sanity (без prod-touches): VM running confirmed, port-scan на хосте показал :18090 и :8089 свободны, traefik dir contains 4 серии bak-stamp'ов сохранённых от attempt 1, IIS sites/pools (snolla, stostayer, stostayer.old) живы.
  2. Backend port: :8089 (recipe-A — НЕ :80, НЕ совпадает с traefik publish :8000/:4443/:8080, НЕ совпадает с VM NAT forwards :18080/:18180/:18181). Выбор user'а (был кандидат :18090, user предпочёл :8089).
  3. Atomic revert plan ДО старта (recipe-E): новая bak-серия .bak-pre-attempt2-2026-05-19 для всех 13 yml (11 cms + 2 stayer), paste-ready команда восстановления записана в .tasks/STATUS.md.
  4. IIS binding snolla *:8089 добавлен через elevated PS (New-WebBinding + Stop-Website; Start-Website — recipe-A note: без restart binding не активируется).
  5. Loop-detect через docker exec (recipe-A + recipe-F): docker exec traefik wget --spider -S --header="Host: emspb.ru" http://host.docker.internal:8089/ → first response 301 → http://www.emspb.ru/ от Server: Microsoft-IIS/10.0. host.docker.internal resolves to 192.168.65.254:8089 — это Docker Desktop host gateway, не traefik publish-port. NO NAT loop. См. docker-host-loopback-detect про общую технику.
  6. Smoke test с -MaximumRedirection 0 (recipe-B): curl.exe -k -I --max-redirs 0 -H "Host: emspb.ru" https://localhost:4443/ → first response 301, Server: Microsoft-IIS/10.0, Location: http://www.emspb.ru/. Это expected CMS canonical redirect (та же логика что была на VM).
  7. Canary atomic — patched ОДИН yml (emspb.yml), file-provider auto-reload, public smoke clean → 📱 phone-test с мобильного интернета (recipe-C) → https://emspb.ru/ открылось → .
  8. Second canarylabtools.yml, smoke + phone-test → .
  9. Batch patch оставшихся 9 cms yml (snolla, rimiz, labtoolspro, pilorama98, tandemmebel, kupimknigi, maljarka, sestech, isc-artmaterials) одним PS-блоком (recipe-G: batch не item-by-item) → smoke 20 hostnames → все Server: Microsoft-IIS/10.0 .
  10. Stayer routes — disabled (re-confirmed Phase 7 decision): user подтвердил «stayer'ы локальные, через traefik наружу не светят». Rename stostayer.yml → stostayer.yml.disabled, oldstostayer.yml → oldstostayer.yml.disabled. docker restart traefik (file-provider не подхватил deletion auto-reload). Verify: stostayer.snolla.com/old.stostayer.ru404 text/plain (traefik no-route). Host IIS sites :8090/:8091 остаются live для прямого/локального доступа.
  11. VM running parallel (recipe-D): VM НЕ savestate'ить минимум 24h+. Освободит RAM/disk только после подтверждённой стабильности host'а.

Pitfall found and resolved

  • WinHTTP proxy strips Server header. Локальный curl.exe http://localhost:8090/ returned response без Server: Microsoft-IIS/10.0 и без X-Powered-By: ASP.NET (плюс с Proxy-Connection: keep-alive) — выглядело как «не IIS отвечает». Это привело к moment'у panic. Но docker exec traefik wget ... (real production path) показал headers корректно. Вывод: для loop-detect / IIS-confirmation тестов использовать docker exec из traefik container, не Windows curl.exe — последний ходит через Windows-уровневый proxy который headers вырезает.

My mistake during this session

  • Я неверно интерпретировал «мигрировать stayer'ов» как «patch traefik backend → host:8090/8091» (т.е. пускать через traefik наружу). User имел в виду «host IIS уже на :8090/:8091, traefik routes должны быть DISABLED». Сделал prod-changing patch → user интервент-stop → revert + rename .yml.disabled. См. memory feedback-migrate-semantics. Lesson — переспрашивать semantics для internal/low-traffic сервисов перед prod-changing.

Финальный chain (после attempt 2)

Клиент (browser)
  → DNS → 94.19.247.14 (public IP)
  → OpenWRT NAT 443 → 192.168.1.143:4443
  → traefik:4443 (TLS termination)
      → match Host → одно из 11 cms-yml → backend
  → http://host.docker.internal:8089/
       ↳ Docker Desktop host gateway 192.168.65.254:8089
  → Windows host IIS site `snolla` (*:80 + *:8089 bindings)
       → C:\sites\snolla\, .NET Framework 4.8.1, ApplicationPoolIdentity
       → conn → MSSQL container на host:1433
  ← HTTP response

Stayer chain: traefik routes disabled. stostayer.snolla.com / old.stostayer.ru → traefik 404. Host IIS sites :8090/:8091 живут для локального доступа.

VM snolla-recovery: running parallel (24h+ soak), backend в traefik больше не используется — VM NAT port forwards (:18080/:18180/:18181) холостые.

Артефакты этой сессии

  • .tasks/STATUS.md — статус task'а 🔴 active.
  • traefik backups в data/custom/: *.yml.bak-pre-attempt2-2026-05-19 для 13 yml, плюс stostayer.yml.disabled, oldstostayer.yml.disabled.
  • feedback-migrate-semantics memory — урок для будущих сессий.
  • concepts/docker-host-loopback-detect.md — новый concept для loop-detect technique.

Что осталось open

  • 24h+ soak до savestate VM (минимум до утра 2026-05-20, лучше 48h до 2026-05-21).
  • VM cleanup: после soak — savestate (освободит RAM), позже unregistervm --delete (освободит ~92 GB).
  • Port forwards OpenWRT (host:18080/18180/18181/18189 → VM) — больше не нужны, удалить позже.
  • rimiz.ru / ics-artmaterials.com 404 — CMS-side routing issues, не инфра. Открытым.
  • X-Forwarded headers (:4443 в redirect URL) — известный bug traefik-on-windows-docker-desktop Pitfall 5, отдельная задача.

Phase 11 — close-out 2026-05-21 (~36h soak)

Soak window 2026-05-19 22:00 MSK → 2026-05-21 ~08:00 MSK ≈ 36 hours. Verify:

  • Traefik: Up 38 hours (docker ps) — zero restarts, zero unscheduled bouncing.
  • w3wp.exe (IIS worker для snolla site): PID 10432, CreationDate 2026-05-20 23:19:21 MSK, uptime 8.8h. Это scheduled IIS app pool recycle (default 1740 min ≈ 29h), не crash — нормальное поведение pool'а после ~29h работы. Память 522 MB.
  • HTTPS smoke 11 заявленных hosts через docker exec traefik wget --spider --server-response https://<host>/:
Host Code Server Verdict
emspb.ru 301→www Microsoft-IIS/10.0
snolla.com 301→on.snolla Microsoft-IIS/10.0
on.snolla.com 200 Microsoft-IIS/10.0
pilorama98.ru 301→www Microsoft-IIS/10.0
labtools.ru 301→www Microsoft-IIS/10.0
labtools.pro 301→www Microsoft-IIS/10.0
tandemmebel.ru 301→www Microsoft-IIS/10.0
kupimknigi.spb.ru 200 Microsoft-IIS/10.0
maljarka.ru DNS bad address ⚠️ off-infra (nslookup 8.8.8.8 → no A record)
sestech.ru TLS cert CN=*.domainparking.ru (expired 2026-05-13) ⚠️ off-infra (domain parking provider)
ics-artmaterials.com 200 nginx-reuseport/1.21.1 ⚠️ off-infra (A→87.236.16.28, мигрирован на сторонний WP-хостинг)

Итог: 8/8 наших sites зеленые. 3 hostname'а из исходного списка task'а оказались уже не на нашей инфраструктуре — DNS либо снят, либо переключен на сторонние хостинги. Это не regression миграции; эти domains надо просто убрать из traefik/IIS routing'а как dead.

Decisions:

  • iis-host migration close-out: DONE 2026-05-21.
  • VM snolla-recovery: готова к savestate (soak passed, zero rollbacks). User-side savestate решает отдельно.
  • 3 off-infra domains: новая задача — iis-traefik-dead-routes-cleanup (low prio).

Артефакты Phase 11

  • .tasks/STATUS.md — task → 🟢 done.
  • (existing) traefik bak-серия .bak-pre-attempt2-2026-05-19 — оставить ещё неделю (atomic revert на случай unforeseen regression).