70824c2afb
Consolidates duplicate KIS API client implementations (governance tests were exercising an unused class instead of the one actually running in production), closes a SQL injection path in the DB admin page, fixes a migration that used MySQL-only syntax and had never actually applied (confirmed against production), resyncs docs/db/quantengine.dbml with all migrations, and removes a duplicate OMS·WMS·ERP frontend tree in favor of src/frontend/. Also corrects several unverifiable/inflated claims in the OMS planning docs and realigns CI/CD and architecture documentation with what's actually in the repo. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
338 lines
12 KiB
Markdown
338 lines
12 KiB
Markdown
# QuantEngine Deployment Runbook
|
|
|
|
Full deployment procedure, extracted from CLAUDE.md (2026-07-30) to keep the main file within
|
|
the character budget. CLAUDE.md keeps the CRITICAL rules (CI/CD-only mandate, DB secret
|
|
management); this file has the complete step-by-step runbook.
|
|
|
|
**Production Server**: Hetzner Cloud `178.104.200.7` (kjh2064@178.104.200.7)
|
|
|
|
Projects on server:
|
|
1. **TaxBaik** (홈페이지) — Nginx location `/taxbaik`
|
|
2. **QuantEngine** (데이터 수집/분석) — Nginx location `/quantengine`
|
|
|
|
## ⚠️ CRITICAL: CI/CD-Only Deployment Mandate
|
|
|
|
**Rule**: ALL production deployments MUST go through Gitea Actions CI/CD. Manual SSH deployments are **FORBIDDEN**.
|
|
|
|
**Why**:
|
|
- Automatic validation (build, health checks, version verification)
|
|
- Audit trail (all deployments logged in Gitea Actions)
|
|
- Consistent process (no manual errors)
|
|
- Rollback safety (deployment history retained)
|
|
- Release traceability (version control via git tags)
|
|
|
|
## ⚠️ CRITICAL: DB Secret Management (Incident 2026-07-12)
|
|
|
|
**Incident**: `quant.taxbaik.com/login`이 `28P01 password authentication failed`로 장애 발생.
|
|
원인: `appsettings.Production.json`에 하드코딩되어 배포된 DB 비밀번호가, 실제 DB 비밀번호가
|
|
로테이션된 이후에도 계속 옛날 값(심지어 이전 세션에서 검증 없이 넣은 placeholder였던 적도 있음)
|
|
그대로 배포되고 있었음.
|
|
|
|
**Rule**: **DB 접속 문자열(`ConnectionStrings`)은 절대 `appsettings.Production.json`이나
|
|
워크플로우 파일에 하드코딩하지 않는다.** `prepare-release.yml`이 생성하는
|
|
`appsettings.Production.json`에는 `Logging` 설정만 있고 `ConnectionStrings`는 없다 —
|
|
이는 의도된 설계다 (Gitea Release는 누구나 다운로드 가능한 아티팩트이므로 시크릿을
|
|
담으면 안 됨).
|
|
|
|
**실제 DB 비밀번호의 출처**: 프로덕션 서버의 `/home/kjh2064/.config/quantengine.env`
|
|
파일 (`ConnectionStrings__DefaultConnection=...` 형식) 하나뿐이며,
|
|
`quantengine.service.d/env.conf` drop-in의 `EnvironmentFile=` 지시자로 systemd가
|
|
이 값을 환경변수로 주입한다. ASP.NET Core 설정 우선순위상 **환경변수가
|
|
`appsettings.Production.json`을 오버라이드**하므로, 배포되는 아티팩트 자체에는
|
|
DB 정보가 없어도 서비스는 정상 동작한다.
|
|
|
|
**DB 비밀번호가 바뀌면** (로테이션 등): `/home/kjh2064/.config/quantengine.env` 파일만
|
|
갱신하고 `sudo systemctl restart quantengine`. 워크플로우 파일이나 Gitea Secrets는
|
|
건드릴 필요 없음 (배포 파이프라인은 DB 비밀번호를 모른 채로 동작해야 정상).
|
|
|
|
**배포 전 체크리스트에 추가**:
|
|
- ✅ 새 릴리즈 배포 후 반드시 `/Account/Login` 실제 HTTP 응답 + `journalctl -u quantengine`에서
|
|
`28P01`/`password authentication failed` 부재 확인 (단순 프로세스 `active` 상태만으로는
|
|
DB 연결 실패를 못 잡음 — ASP.NET Core는 DB 없이도 기동은 되고 로그인 요청 시점에야 실패함)
|
|
- ✅ `.config/quantengine.env`의 존재와 `quantengine.service.d/env.conf`의
|
|
`EnvironmentFile=` 배선이 서버에 유지되고 있는지 (systemd unit 자체를 재생성/덮어쓰는
|
|
배포 방식으로 전환할 경우 이 drop-in이 날아가지 않는지 확인 필요)
|
|
|
|
## Production Deployment Strategy (Release-Based)
|
|
|
|
**Architecture**: Two-Workflow System (Release Creation → Deployment)
|
|
|
|
### Workflow 1: prepare-release.yml (Release Creation)
|
|
|
|
**Purpose**: Create a release with built artifact
|
|
|
|
**Trigger**: Manual (`workflow_dispatch`)
|
|
```bash
|
|
# Visit Gitea Actions and select prepare-release.yml
|
|
# Input version: v0.1.20260711 (or any semantic version)
|
|
```
|
|
|
|
**What it does**:
|
|
1. ✓ Build (restore, build, publish)
|
|
2. ✓ Generate `appsettings.Production.json`
|
|
3. ✓ Package artifact: `.tar.gz`
|
|
4. ✓ Create git tag: `v0.1.20260711`
|
|
5. ✓ Create Gitea Release with artifact attached
|
|
6. ✓ Notify: Release ready for deployment
|
|
|
|
**Output**: Gitea Release with downloadable artifact
|
|
|
|
### Workflow 2: deploy-prod.yml (Deployment)
|
|
|
|
**Purpose**: Deploy a release to production
|
|
|
|
**Trigger**: Manual (`workflow_dispatch`)
|
|
```bash
|
|
# Visit Gitea Actions and select deploy-prod.yml
|
|
# Input release: v0.1.20260711 (optional — uses latest if empty)
|
|
```
|
|
|
|
**What it does**:
|
|
1. ✓ Fetch Release (from Gitea Releases)
|
|
2. ✓ Download artifact
|
|
3. ✓ Verify SSH credentials
|
|
4. ✓ Upload to production server
|
|
5. ✓ Extract and symlink
|
|
6. ✓ Restart service
|
|
7. ✓ 6-point health checks
|
|
8. ✓ Report deployment status
|
|
|
|
**Deployment Pipeline (5 Stages)**:
|
|
|
|
| Stage | Purpose | Timeout |
|
|
|-------|---------|---------|
|
|
| 1. Fetch Release | Query Gitea Releases, download artifact | 10min |
|
|
| 2. Pre-Check | Verify SSH keys, secrets, release | 5min |
|
|
| 3. Deploy | Upload, extract, symlink, restart service | 30min |
|
|
| 4. Health Check | 6-point verification (HTTP, CSS, login, service, release, DB auth) | 10min |
|
|
| 5. Report | Final deployment status | Auto |
|
|
|
|
**Health Checks (Automatic)**:
|
|
- ✓ HTTP 200 on `/Account/Login`
|
|
- ✓ Login page content verification
|
|
- ✓ CSS file loads (`/css/admin.css`)
|
|
- ✓ Service status (systemctl active)
|
|
- ✓ Release verification (deployed release tag matches)
|
|
- ✓ **DB authentication check** (`journalctl`에서 `28P01`/`password authentication failed`
|
|
부재 확인 — GET `/Account/Login`은 DB가 끊겨도 200을 반환하므로 이 체크가 없으면
|
|
DB 장애를 배포 파이프라인이 놓친다. 2026-07-12 사고 이후 추가됨)
|
|
|
|
**Complete Deployment Flow**:
|
|
```
|
|
1. Code committed to main branch
|
|
2. Create release: prepare-release.yml workflow_dispatch (manual)
|
|
→ Builds code
|
|
→ Creates Gitea Release with artifact
|
|
→ Tags repository
|
|
3. Deploy release: deploy-prod.yml workflow_dispatch (manual)
|
|
→ Selects release version
|
|
→ Downloads artifact from Gitea Release
|
|
→ Deploys to production server
|
|
→ Runs health checks
|
|
→ Reports status
|
|
```
|
|
|
|
## Pre-Deployment Checklist
|
|
|
|
**Before creating a release**, verify:
|
|
1. ✅ Local build: `dotnet build src/dotnet/QuantEngine.Web/QuantEngine.Web.csproj -c Release` (0 errors, 0 warnings)
|
|
2. ✅ E2E tests pass: `npx playwright test`
|
|
3. ✅ Admin pages verified (200 status, no 500 errors)
|
|
4. ✅ All changes committed and pushed to main branch
|
|
5. ✅ No uncommitted changes: `git status`
|
|
|
|
## Release & Deployment Workflow
|
|
|
|
**Step 1: Create Release (prepare-release.yml)**
|
|
```bash
|
|
# Visit Gitea Actions
|
|
# https://gitea.taxbaik.com/kjh2064/QuantEngineByItz/actions
|
|
|
|
# Run prepare-release.yml workflow
|
|
# Input: version = v0.1.20260711
|
|
|
|
# Workflow will:
|
|
# - Build and publish
|
|
# - Package artifact
|
|
# - Create git tag
|
|
# - Create Gitea Release
|
|
# - Attach artifact
|
|
```
|
|
|
|
**Step 2: Deploy Release (deploy-prod.yml)**
|
|
```bash
|
|
# Visit Gitea Actions (same page)
|
|
# Run deploy-prod.yml workflow
|
|
# Input: release = v0.1.20260711 (leave empty for latest)
|
|
|
|
# Workflow will:
|
|
# - Download artifact from release
|
|
# - Deploy to production server
|
|
# - Run health checks
|
|
# - Report status
|
|
```
|
|
|
|
## SSH Key Configuration (Required)
|
|
|
|
**Setup (One-time)**:
|
|
1. Generate ED25519 key locally (or reuse existing):
|
|
```bash
|
|
ssh-keygen -t ed25519 -f ~/.ssh/quantengine_deploy -C "QuantEngine CI/CD"
|
|
```
|
|
|
|
2. Add public key to production server:
|
|
```bash
|
|
ssh-copy-id -i ~/.ssh/quantengine_deploy.pub kjh2064@178.104.200.7
|
|
```
|
|
|
|
3. Get private key in base64 format:
|
|
```bash
|
|
# macOS/Linux
|
|
base64 -w 0 ~/.ssh/quantengine_deploy > /tmp/key_b64.txt
|
|
cat /tmp/key_b64.txt | pbcopy
|
|
|
|
# Or Windows PowerShell
|
|
$key = Get-Content ~/.ssh/quantengine_deploy -Raw
|
|
[Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($key)) | Set-Clipboard
|
|
```
|
|
|
|
4. Configure in Gitea:
|
|
- URL: https://gitea.taxbaik.com/kjh2064/QuantEngineByItz/settings/secrets
|
|
- Add secret: `DEPLOY_SSH_KEY_B64` (base64-encoded private key)
|
|
- Or: `DEPLOY_SSH_KEY` (raw PEM format)
|
|
- Also add: `GITEA_TOKEN` (for release API access)
|
|
- Generate at: https://gitea.taxbaik.com/user/settings/applications
|
|
- Required permissions: `repo` + `read:actions`
|
|
|
|
## Deployment Monitoring
|
|
|
|
**During Deployment**:
|
|
- Watch live in Gitea Actions UI
|
|
- Jobs complete in order: Build → Pre-Check → Deploy → Health Check → Report
|
|
|
|
**After Deployment**:
|
|
```bash
|
|
# SSH into server
|
|
ssh kjh2064@178.104.200.7
|
|
|
|
# Check active deployment
|
|
readlink ~/quantengine_active
|
|
|
|
# View service status
|
|
systemctl status quantengine
|
|
|
|
# Tail live logs
|
|
journalctl -u quantengine -f
|
|
|
|
# Health check
|
|
curl -I http://127.0.0.1:5000/Account/Login
|
|
```
|
|
|
|
## Automatic Rollback (if health check fails)
|
|
|
|
If health check fails, deployment stops automatically:
|
|
1. Service restart may fail
|
|
2. Symlink update reverts to previous deployment
|
|
3. Gitea Actions marks deployment as FAILED
|
|
4. Logs include failure details
|
|
|
|
Manual rollback (if needed):
|
|
```bash
|
|
# List deployments
|
|
ls -lht ~/deployments/quantengine_*
|
|
|
|
# Revert symlink to previous version
|
|
ln -sfn /home/kjh2064/deployments/quantengine_YYYYMMDD_HHMMSS_COMMIT ~/quantengine_active
|
|
|
|
# Restart service
|
|
sudo systemctl restart quantengine
|
|
|
|
# Verify
|
|
curl http://127.0.0.1:5000/Account/Login
|
|
```
|
|
|
|
## Troubleshooting Deployment Failures
|
|
|
|
**Issue**: Build fails
|
|
- Check: `dotnet build` locally first
|
|
- Ensure: No compilation errors, 0 warnings
|
|
|
|
**Issue**: Health check timeout
|
|
- Check: Service logs: `journalctl -u quantengine -n 50`
|
|
- Check: Port 5000 listening: `ss -tlnp | grep 5000`
|
|
- Check: DB connectivity in appsettings.Production.json
|
|
|
|
**Issue**: SSH key error
|
|
- Verify: `DEPLOY_SSH_KEY_B64` or `DEPLOY_SSH_KEY` in Gitea Secrets
|
|
- Check: Public key added to `~/.ssh/authorized_keys` on server
|
|
- Test: `ssh -i ~/.ssh/key_file kjh2064@178.104.200.7 echo OK`
|
|
|
|
## Git Repository
|
|
|
|
**Gitea Server** (동일 호스트):
|
|
- **HTTP**: `https://gitea.taxbaik.com/kjh2064/QuantEngineByItz.git`
|
|
- **SSH**: `ssh://git@gitea.taxbaik.com:2222/kjh2064/QuantEngineByItz.git`
|
|
|
|
## Active Gitea Workflows (summary)
|
|
|
|
1. **prepare-release.yml** — Release creation (workflow_dispatch only)
|
|
- Build → Publish → Package → Tag → Gitea Release
|
|
- Does NOT write ConnectionStrings into the artifact (see DB Secret Management above) —
|
|
only `Logging` config ships in `appsettings.Production.json`
|
|
2. **deploy-prod.yml** — Production deployment (workflow_dispatch only, takes a release tag)
|
|
- 5 stages: Fetch Release → Pre-Check → Deploy → Health Check → Report
|
|
- 6-point health checks (HTTP, login page, CSS, service, release, DB auth)
|
|
- SSH-based deployment with artifact validation
|
|
3. **ci.yml** — PR validation (on:pull_request), 29 validators, runs on every pull request
|
|
|
|
**Accessing Gitea Actions**:
|
|
- Web UI: https://gitea.taxbaik.com/kjh2064/QuantEngineByItz/actions
|
|
- Runs API: https://gitea.taxbaik.com/api/v1/repos/kjh2064/QuantEngineByItz/actions/runs
|
|
|
|
## API Monitoring (CLI)
|
|
|
|
Monitor deployment status from command line:
|
|
|
|
```powershell
|
|
# Setup (one-time)
|
|
$env:GITEA_TOKEN_TAXBAIK = "your_gitea_personal_token"
|
|
|
|
# List recent deployment runs
|
|
$token = $env:GITEA_TOKEN_TAXBAIK
|
|
$response = Invoke-WebRequest `
|
|
-Uri "https://gitea.taxbaik.com/api/v1/repos/kjh2064/QuantEngineByItz/actions/runs?limit=5" `
|
|
-Headers @{ "Authorization" = "token $token" }
|
|
($response.Content | ConvertFrom-Json).workflow_runs | ForEach-Object {
|
|
Write-Host "Run #$($_.id): $($_.display_title) [$($_.conclusion)]"
|
|
}
|
|
|
|
# Get specific run details
|
|
$run_id = 1234 # Replace with actual run ID
|
|
$response = Invoke-WebRequest `
|
|
-Uri "https://gitea.taxbaik.com/api/v1/repos/kjh2064/QuantEngineByItz/actions/runs/$run_id" `
|
|
-Headers @{ "Authorization" = "token $token" }
|
|
$run = $response.Content | ConvertFrom-Json
|
|
Write-Host "Commit: $($run.head_sha)"
|
|
Write-Host "Status: $($run.status) / $($run.conclusion)"
|
|
```
|
|
|
|
See `docs/GITEA_ACTIONS_API_GUIDE.md` for the complete API reference.
|
|
|
|
## Deployment Secrets Configuration
|
|
|
|
**Required Secrets** (Gitea Repository Settings → Secrets):
|
|
|
|
| Secret | Type | Purpose |
|
|
|--------|------|---------|
|
|
| `DEPLOY_SSH_KEY_B64` | Base64 (recommended) | ED25519 private key for SSH |
|
|
| `DEPLOY_SSH_KEY` | PEM (alternative) | Raw private key format |
|
|
| `DEPLOY_HOST` | Text | Production server IP (178.104.200.7) |
|
|
| `DEPLOY_USER` | Text | SSH username (kjh2064) |
|
|
|
|
**How to add secrets**:
|
|
1. Go to: https://gitea.taxbaik.com/kjh2064/QuantEngineByItz/settings/secrets
|
|
2. Click "Add Secret"
|
|
3. Name: `DEPLOY_SSH_KEY_B64`
|
|
4. Value: `base64 -w 0 ~/.ssh/deploy_key | pbcopy` (macOS) or `certutil -encode deploy_key deploy_key.b64` (Windows)
|
|
5. Save
|