[CLAUDE] Skill: thêm 3 skill ops project-specific
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 2m47s
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 2m47s
Khảo sát alirezarezvani/claude-skills repo — phần lớn skill đã có ở user-level (code-reviewer, sql-database-assistant, focused-fix, senior-frontend, mcp-builder...). Bulk import sẽ trùng + nhiều skill là doc-dump generic không có YAML when-to-use. Thay vào đó: viết 3 skill PROJECT-SPECIFIC encode kiến thức SOLUTION_ERP-only mà generic không thể biết: - dependency-audit-erp: dotnet list --vulnerable + npm audit cho fe-admin/fe-user, respect pin constraint MediatR 12.4.1 + Swashbuckle 6.9.0 + Node 20.x, dẫn chiếu gotchas, output template + CI integration TODO Phase 5.1 - ef-core-migration: 8 migration history + 3-file rule + Design TimeDbContextFactory + 6 pitfalls cụ thể (bao gồm cascade vs restrict cho WorkflowDefinitionId), workflow add entity mới end- to-end, prod apply via idempotent script - iis-deploy-runbook: 3 IIS site topology + win-acme cert + NSSM gitea-runner shared VIETREPORT + LibreOffice 25.8.6 headless, debug playbook 500/502/SignalR/login, deploy steps + manual emergency, rotate creds + backup commands, dẫn chiếu gotcha #25/26/28/29 Skills README cập nhật: 6 skill (3 domain + 3 ops). CLAUDE.md + docs/CLAUDE.md sync count. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
332
.claude/skills/iis-deploy-runbook/SKILL.md
Normal file
332
.claude/skills/iis-deploy-runbook/SKILL.md
Normal file
@ -0,0 +1,332 @@
|
||||
---
|
||||
name: iis-deploy-runbook
|
||||
description: Ops runbook cho SOLUTION_ERP deploy trên Windows Server IIS — 3 site (api/admin/user.huypham.vn), win-acme Let's Encrypt, NSSM gitea-runner shared với VIETREPORT, LibreOffice soffice headless. Dùng khi debug 500/502 prod, restart site, rotate cert, fix CI/CD runner, troubleshoot WebSocket, thêm site mới.
|
||||
when-to-use:
|
||||
- "prod 500 error"
|
||||
- "IIS site fail"
|
||||
- "cert hết hạn"
|
||||
- "win-acme"
|
||||
- "gitea runner"
|
||||
- "deploy IIS"
|
||||
- "restart app pool"
|
||||
- "webSocket 500"
|
||||
- "reverse proxy FE"
|
||||
- "LibreOffice prod"
|
||||
---
|
||||
|
||||
# IIS Deploy Runbook — SOLUTION_ERP
|
||||
|
||||
> **Context:** VPS Windows Server shared với VIETREPORT project. IIS + URL Rewrite + ARR + WebSockets module + win-acme. Deploy qua Gitea Actions self-hosted runner.
|
||||
|
||||
## Production topology
|
||||
|
||||
```
|
||||
Internet
|
||||
│ 443 (HTTPS)
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ IIS (Windows Server VPS) │
|
||||
│ │
|
||||
│ ┌─ api.huypham.vn ─┐ ┌─ admin.huypham.vn ─┐ ┌─ user.huypham.vn ─┐
|
||||
│ │ SolutionErp-Api │ │ SolutionErp-Admin │ │ SolutionErp-User │
|
||||
│ │ → out-of-process │ │ (static SPA, URL │ │ (static SPA, URL │
|
||||
│ │ Kestrel :5443 │ │ Rewrite /api → 5443)│ │ Rewrite...) │
|
||||
│ │ ASP.NET Core 10 │ │ React build/ │ │ React build/ │
|
||||
│ │ │ │ │ │ │
|
||||
│ └────────────────────┘ └────────────────────┘ └────────────────────┘
|
||||
│ │
|
||||
│ Let's Encrypt (win-acme) — 3 cert auto-renew 60d │
|
||||
│ Shared gitea-runner NSSM service (with VIETREPORT) │
|
||||
│ LibreOffice 25.8.6 headless │
|
||||
│ SQL Server 2019 Express (\\.\SQLEXPRESS) │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 3 IIS sites
|
||||
|
||||
| Site | Binding | Physical path | Apool | Purpose |
|
||||
|---|---|---|---|---|
|
||||
| `SolutionErp-Api` | `*:443:api.huypham.vn` HTTPS | `C:\inetpub\apps\SolutionErp\Api\` | out-of-process Kestrel | ASP.NET Core 10 API (port 5443 internal) |
|
||||
| `SolutionErp-Admin` | `*:443:admin.huypham.vn` HTTPS + `*:80` redirect | `C:\inetpub\apps\SolutionErp\Admin\` | static (no app pool .NET) | React build fe-admin |
|
||||
| `SolutionErp-User` | `*:443:user.huypham.vn` HTTPS + `*:80` redirect | `C:\inetpub\apps\SolutionErp\User\` | static | React build fe-user |
|
||||
|
||||
**SPA web.config:** 2 FE có `URL Rewrite` rule:
|
||||
1. HTTP → HTTPS redirect (bắt buộc, CORS whitelist chỉ https)
|
||||
2. `/api/* → http://localhost:5443/api/*` (ARR reverse proxy)
|
||||
3. `/hubs/* → http://localhost:5443/hubs/*` (SignalR)
|
||||
4. React Router fallback: `/*` → `/index.html`
|
||||
|
||||
## Quick commands
|
||||
|
||||
### Restart 1 site
|
||||
|
||||
```powershell
|
||||
# PowerShell as Admin
|
||||
Import-Module WebAdministration
|
||||
Stop-WebSite -Name "SolutionErp-Api"
|
||||
Start-WebSite -Name "SolutionErp-Api"
|
||||
|
||||
# Hoặc recycle app pool (API out-of-process):
|
||||
Restart-WebAppPool -Name "SolutionErp-Api"
|
||||
|
||||
# Check site status:
|
||||
Get-Website -Name "SolutionErp-*" | Format-Table Name, State, Bindings
|
||||
```
|
||||
|
||||
### Xem log API
|
||||
|
||||
```powershell
|
||||
# Serilog file rolling daily
|
||||
Get-Content "C:\inetpub\apps\SolutionErp\Api\Logs\log-$(Get-Date -Format 'yyyyMMdd').txt" -Tail 50
|
||||
|
||||
# IIS log
|
||||
Get-Content "C:\inetpub\logs\LogFiles\W3SVC<ID>\u_ex$(Get-Date -Format 'yyMMdd').log" -Tail 30
|
||||
|
||||
# Stdout log khi crash startup
|
||||
Get-Content "C:\inetpub\apps\SolutionErp\Api\Logs\stdout_*.log" -Tail 30
|
||||
```
|
||||
|
||||
### Health check
|
||||
|
||||
```powershell
|
||||
# Từ server
|
||||
curl http://localhost:5443/health/live
|
||||
curl http://localhost:5443/health/ready
|
||||
|
||||
# Từ ngoài
|
||||
curl https://api.huypham.vn/health/ready
|
||||
```
|
||||
|
||||
## Let's Encrypt cert — win-acme
|
||||
|
||||
### Check trạng thái
|
||||
|
||||
```powershell
|
||||
# Mở win-acme interactive
|
||||
& "C:\tools\win-acme\wacs.exe"
|
||||
# Menu > Manage renewals > list — xem 3 cert + next renew date
|
||||
|
||||
# Hoặc file:
|
||||
Get-Content "C:\ProgramData\win-acme\Production\$(hostname)\Renewals\*.renewal.json"
|
||||
```
|
||||
|
||||
### Cert hết hạn emergency
|
||||
|
||||
```powershell
|
||||
# Force renew 1 cert
|
||||
& "C:\tools\win-acme\wacs.exe" --renew --force --id {renewal-id}
|
||||
|
||||
# Full re-issue nếu renewal fail:
|
||||
& "C:\tools\win-acme\wacs.exe" # interactive → 'N' create new
|
||||
# Chọn: HTTP validation, web root = site physical path, auto install IIS
|
||||
```
|
||||
|
||||
**Gotcha:** Shared runner với VIETREPORT → win-acme HTTP challenge cần `.well-known/acme-challenge/` accessible qua HTTP (port 80). Rule HTTP→HTTPS redirect trong web.config PHẢI **exclude** path này:
|
||||
|
||||
```xml
|
||||
<rule name="Redirect to HTTPS" stopProcessing="true">
|
||||
<match url="(.*)" />
|
||||
<conditions>
|
||||
<add input="{HTTPS}" pattern="off" />
|
||||
<add input="{REQUEST_URI}" pattern="^/\.well-known/" negate="true" />
|
||||
</conditions>
|
||||
<action type="Redirect" url="https://{HTTP_HOST}/{R:1}" />
|
||||
</rule>
|
||||
```
|
||||
|
||||
## Gitea Actions runner (NSSM service)
|
||||
|
||||
### Status
|
||||
|
||||
```powershell
|
||||
# NSSM service name: gitea-runner (shared với VIETREPORT)
|
||||
Get-Service gitea-runner
|
||||
nssm status gitea-runner
|
||||
|
||||
# Restart
|
||||
Restart-Service gitea-runner
|
||||
|
||||
# Log
|
||||
Get-Content "C:\tools\gitea-runner\logs\act_runner.log" -Tail 50
|
||||
```
|
||||
|
||||
### Token rotate (nếu runner disconnected)
|
||||
|
||||
```powershell
|
||||
# Stop service
|
||||
Stop-Service gitea-runner
|
||||
|
||||
# Re-register qua Gitea admin UI → Actions → Runners → get new registration token
|
||||
& "C:\tools\gitea-runner\act_runner.exe" register `
|
||||
--instance https://git.baocaogiaoduc.vn `
|
||||
--token <new-token> `
|
||||
--no-interactive
|
||||
|
||||
# Start lại
|
||||
Start-Service gitea-runner
|
||||
```
|
||||
|
||||
## LibreOffice headless (PDF / docx converter)
|
||||
|
||||
### Check install
|
||||
|
||||
```powershell
|
||||
& "C:\Program Files\LibreOffice\program\soffice.exe" --version
|
||||
# → LibreOffice 25.8.6.x
|
||||
```
|
||||
|
||||
### Test convert manual
|
||||
|
||||
```powershell
|
||||
# Tạo temp dir isolated (mô phỏng per-request pattern của LibreOfficeDocumentConverter)
|
||||
$work = New-Item -ItemType Directory -Path "$env:TEMP\lo-test-$(Get-Random)"
|
||||
$userInst = "$work\userinst"
|
||||
|
||||
& "C:\Program Files\LibreOffice\program\soffice.exe" `
|
||||
--headless `
|
||||
"-env:UserInstallation=file:///$($userInst.Replace('\', '/'))" `
|
||||
--convert-to pdf `
|
||||
--outdir $work `
|
||||
"C:\path\to\test.docx"
|
||||
|
||||
# Output: $work\test.pdf
|
||||
ls $work
|
||||
Remove-Item -Recurse -Force $work
|
||||
```
|
||||
|
||||
### Prod fail patterns
|
||||
|
||||
- **60s timeout** → PDF lớn (>100 page) có thể quá. Xem `LibreOfficeDocumentConverter` — tăng timeout nếu cần
|
||||
- **Locked font fallback** → Be Vietnam Pro missing → text render hỏng. Install font trên server
|
||||
- **Concurrent request lock** → mỗi request 1 `UserInstallation` dir riêng → tránh lock
|
||||
|
||||
## Debug playbook — prod error
|
||||
|
||||
### HTTP 500 all site
|
||||
|
||||
Xem gotcha #25 (docs/gotchas.md):
|
||||
```powershell
|
||||
# Likely config lock:
|
||||
& "$env:SystemRoot\system32\inetsrv\appcmd.exe" list config -section:system.webServer/webSocket
|
||||
# → overrideMode="Deny" → fix:
|
||||
& "$env:SystemRoot\system32\inetsrv\appcmd.exe" unlock config -section:system.webServer/webSocket
|
||||
```
|
||||
|
||||
### HTTP 502 Bad Gateway (Admin/User → API)
|
||||
|
||||
```
|
||||
1. Check API up: curl http://localhost:5443/health/live
|
||||
- Down → restart API site + check stdout log
|
||||
2. Check ARR enabled: IIS Manager > server level > Application Request Routing
|
||||
- "Enable proxy" phải tick
|
||||
3. Check URL Rewrite rule fe web.config
|
||||
- action type="Rewrite" url="http://localhost:5443/{R:0}"
|
||||
```
|
||||
|
||||
### SignalR 401 (WebSocket connect fail)
|
||||
|
||||
Xem gotcha #26:
|
||||
```
|
||||
1. FE console: check ?access_token= query có trong negotiate URL không
|
||||
2. BE log: JwtBearer OnMessageReceived có fire cho /hubs/* không
|
||||
3. IIS WebSocket module: Install-WindowsFeature Web-WebSockets (đã có)
|
||||
4. Section unlock: appcmd unlock config -section:system.webServer/webSocket
|
||||
```
|
||||
|
||||
### Login "Network Error"
|
||||
|
||||
Xem `docs/gotchas.md` CORS + HTTPS redirect:
|
||||
```
|
||||
1. User gõ http://admin.huypham.vn → không redirect → CORS block
|
||||
2. Fix: SPA web.config PHẢI có HTTP→HTTPS rule (đã có)
|
||||
3. Test: curl -I http://admin.huypham.vn → expect 301 Location: https://...
|
||||
```
|
||||
|
||||
### DB connection fail
|
||||
|
||||
```powershell
|
||||
# 1. SQL service up?
|
||||
Get-Service MSSQL*
|
||||
|
||||
# 2. TCP enabled?
|
||||
Import-Module SqlServer
|
||||
# Hoặc check registry:
|
||||
Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\MSSQL*.SQLEXPRESS\MSSQLServer\SuperSocketNetLib\Tcp"
|
||||
|
||||
# 3. vrapp login OK?
|
||||
sqlcmd -S .\SQLEXPRESS -U vrapp -P <pw> -Q "SELECT DB_NAME()"
|
||||
# Expect: SolutionErp
|
||||
|
||||
# 4. appsettings connection string (qua Gitea secrets)
|
||||
# Check C:\inetpub\apps\SolutionErp\Api\appsettings.Production.json có ConnectionStrings:DefaultConnection
|
||||
```
|
||||
|
||||
## Deploy steps (CI/CD xanh)
|
||||
|
||||
Gitea Actions workflow: `.gitea/workflows/deploy.yml`. Flow:
|
||||
|
||||
```
|
||||
Push to main
|
||||
→ Runner pick up job
|
||||
→ checkout repo
|
||||
→ setup .NET 10 + Node 20
|
||||
→ npm ci (fe-admin + fe-user, rolldown native binding OK nếu fresh node_modules)
|
||||
→ dotnet restore + publish
|
||||
→ npm run build (fe-admin + fe-user)
|
||||
→ render appsettings.Production.json từ secrets (JWT_SECRET, DB_CONNECTION)
|
||||
→ stop app pool SolutionErp-Api
|
||||
→ xcopy publish → C:\inetpub\apps\SolutionErp\{Api,Admin,User}
|
||||
→ start app pool
|
||||
→ curl /health/ready → must be 200 trong 30s
|
||||
→ report status
|
||||
```
|
||||
|
||||
### Manual deploy (emergency)
|
||||
|
||||
```powershell
|
||||
# Local build
|
||||
dotnet publish src/Backend/SolutionErp.Api -c Release -o .\publish\api
|
||||
cd fe-admin; npm ci; npm run build; cd ..
|
||||
cd fe-user; npm ci; npm run build; cd ..
|
||||
|
||||
# Scp sang server (cần plink/pscp hoặc rsync)
|
||||
scp -r .\publish\api\* user@server:C:/inetpub/apps/SolutionErp/Api/
|
||||
scp -r .\fe-admin\dist\* user@server:C:/inetpub/apps/SolutionErp/Admin/
|
||||
scp -r .\fe-user\dist\* user@server:C:/inetpub/apps/SolutionErp/User/
|
||||
|
||||
# Trên server:
|
||||
Restart-WebAppPool -Name "SolutionErp-Api"
|
||||
curl http://localhost:5443/health/ready
|
||||
```
|
||||
|
||||
## Backup + recovery
|
||||
|
||||
```powershell
|
||||
# DB backup (script sẵn, chưa schedule):
|
||||
& "C:\inetpub\apps\SolutionErp\scripts\backup-sql.ps1"
|
||||
# Output: backup/SolutionErp_<ts>.bak (compressed + retention 30d)
|
||||
|
||||
# Schedule daily 03:00:
|
||||
schtasks /create /tn "SolutionErp Backup" `
|
||||
/tr "powershell -ExecutionPolicy Bypass -File C:\inetpub\apps\SolutionErp\scripts\backup-sql.ps1" `
|
||||
/sc DAILY /st 03:00 /ru SYSTEM
|
||||
```
|
||||
|
||||
Restore: xem `docs/guides/runbook.md`.
|
||||
|
||||
## Rotate credentials (Phase 5.1 backlog)
|
||||
|
||||
- [ ] SQL `sa` password (rotate)
|
||||
- [ ] SQL `vrapp` password (update Gitea secret `DB_CONNECTION` + appsettings.Production.json)
|
||||
- [ ] JWT secret (update Gitea secret `JWT_SECRET`, next deploy sẽ lan tỏa. Tất cả token cũ invalid)
|
||||
- [ ] Gitea runner registration token (re-register service)
|
||||
- [ ] Admin default `Admin@123456` (đổi qua `/system/users` admin UI ngay sau deploy)
|
||||
|
||||
## Related
|
||||
|
||||
- `docs/guides/deployment-iis.md` — first-time setup
|
||||
- `docs/guides/runbook.md` — operations guide chi tiết
|
||||
- `docs/guides/cicd.md` — CI/CD pipeline
|
||||
- `docs/gotchas.md` — #25 webSocket lock, #26 SignalR, #28 LibreOffice 404, #29 PS 5.1 UTF-16
|
||||
- `scripts/deploy-iis.ps1` · `scripts/backup-sql.ps1` · `scripts/install-libreoffice.ps1`
|
||||
- `.gitea/workflows/deploy.yml` — CI/CD definition
|
||||
Reference in New Issue
Block a user