# ============================================================== # GUARDiA ITSM 설치 스크립트 — Windows Server 2019/2022 # ============================================================== # 전제조건: 순수 Windows Server OS (PowerShell 5.1+) # 실행 방법: PowerShell -ExecutionPolicy Bypass -File setup_windows.ps1 # 설치 테스트: .\setup_windows.ps1 -Test # ============================================================== param( [switch]$Test = $false ) $ErrorActionPreference = "Stop" $ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path $GuardiaRoot = Split-Path -Parent $ScriptDir $LogFile = "C:\guardia_install.log" function Write-OK { param($msg) Write-Host "[OK] $msg" -ForegroundColor Green } function Write-Warn { param($msg) Write-Host "[WARN] $msg" -ForegroundColor Yellow } function Write-Fail { param($msg) Write-Host "[FAIL] $msg" -ForegroundColor Red; exit 1 } function Write-Info { param($msg) Write-Host " $msg" } Start-Transcript -Path $LogFile -Append | Out-Null Write-Host "==================================================" Write-Host " GUARDiA ITSM 설치 — Windows Server" Write-Host " 시작: $(Get-Date)" Write-Host "==================================================" # ── 테스트 모드 ───────────────────────────────────────────── if ($Test) { Write-Host "=== 설치 검증 모드 ===" $pass = 0; $fail = 0 function Check-Item { param($desc, [scriptblock]$cmd) try { & $cmd | Out-Null Write-OK $desc; $script:pass++ } catch { Write-Host "[FAIL] $desc" -ForegroundColor Red; $script:fail++ } } Check-Item "Python 3.11+" { python --version 2>&1 | Select-String "3\.(1[1-9]|[2-9]\d)" } Check-Item "PostgreSQL 응답" { pg_isready -q } Check-Item "Redis 응답" { redis-cli ping } Check-Item "Java 17 (OpenJDK)" { java -version 2>$null } Check-Item "Tomcat 9 서비스" { $s = Get-Service "tomcat9" -ErrorAction Stop; if ($s.Status -ne "Running") { throw "" } } Check-Item "Tomcat HTTP" { $r = Invoke-WebRequest "http://localhost:8080/" -UseBasicParsing -TimeoutSec 5; if ($r.StatusCode -ne 200) { throw "" } } Check-Item "NSSM 서비스 등록" { Get-Service "guardia-itsm" -ErrorAction Stop } Check-Item "GUARDiA 서비스 실행" { $s = Get-Service "guardia-itsm" if ($s.Status -ne "Running") { throw "서비스 상태: $($s.Status)" } } Check-Item "GUARDiA HTTP 응답" { $r = Invoke-WebRequest "http://localhost:8001/" -UseBasicParsing -TimeoutSec 10 if ($r.StatusCode -ne 200) { throw "HTTP $($r.StatusCode)" } } Check-Item "GUARDiA 로그인 API" { $body = '{"username":"admin","password":"1111"}' $r = Invoke-WebRequest "http://localhost:8001/api/auth/login" -Method POST ` -ContentType "application/json" -Body $body -UseBasicParsing -TimeoutSec 10 if ($r.StatusCode -ne 200) { throw "로그인 실패 ($($r.StatusCode))" } } Check-Item "Python UTF-8 인코딩" { $env:PYTHONIOENCODING = "utf-8" $out = & python -c "print('OK')" 2>&1 if ("$out" -notmatch "OK") { throw "인코딩 오류: $out" } } Check-Item "Ollama 실행" { $r = Invoke-WebRequest "http://localhost:11434/api/version" -UseBasicParsing -TimeoutSec 5 if ($r.StatusCode -ne 200) { throw "Ollama 응답 없음" } } Check-Item "Ollama 모델 존재" { $out = ollama list 2>&1 if ($out -notmatch "\S") { throw "모델 없음" } } Check-Item "Nginx 설정" { nginx -t 2>&1 } Write-Host "" Write-Host "검증 결과: 성공 $pass / 실패 $fail" if ($fail -eq 0) { Write-OK "모든 검사 통과 — GUARDiA ITSM 정상 설치됨" } else { Write-Fail "일부 검사 실패 — 로그 확인: $LogFile" } Stop-Transcript | Out-Null exit 0 } # ── 관리자 권한 확인 ──────────────────────────────────────── $isAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole( [Security.Principal.WindowsBuiltInRole]::Administrator ) if (-not $isAdmin) { Write-Fail "관리자 권한으로 실행하세요 (우클릭 → 관리자로 실행)" } # ── Chocolatey 패키지 관리자 ──────────────────────────────── Write-Host "" Write-Host "[1/8] Chocolatey 패키지 관리자 설치..." if (-not (Get-Command choco -ErrorAction SilentlyContinue)) { Set-ExecutionPolicy Bypass -Scope Process -Force [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072 Invoke-Expression ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1')) $env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User") } Write-OK "Chocolatey 준비 완료" # ── 1. 필수 패키지 설치 ────────────────────────────────────── Write-Host "" Write-Host "[2/10] 필수 패키지 설치 (Python, Java, PostgreSQL, Redis, Nginx, NSSM)..." $packages = @( "python --version=3.11.9", "openjdk --version=17.0.11", "postgresql", "redis-64", "nginx-winssl", "nssm", "git" ) foreach ($pkg in $packages) { $name = $pkg.Split(" ")[0] Write-Host " 설치 중: $name" choco install $pkg -y --no-progress 2>&1 | Out-Null } # PATH 갱신 $env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User") # JAVA_HOME 설정 $javaHome = (Get-ChildItem "C:\Program Files\Eclipse Adoptium" -ErrorAction SilentlyContinue | Sort-Object Name -Descending | Select-Object -First 1)?.FullName if (-not $javaHome) { $javaHome = (Get-ChildItem "C:\Program Files\Java" -Filter "jdk*17*" -ErrorAction SilentlyContinue | Select-Object -First 1)?.FullName } if (-not $javaHome) { $javaHome = (Get-Command java -ErrorAction SilentlyContinue)?.Source | Split-Path | Split-Path } if ($javaHome) { [System.Environment]::SetEnvironmentVariable("JAVA_HOME", $javaHome, "Machine") $env:JAVA_HOME = $javaHome Write-OK "OpenJDK 17 설치 완료 (JAVA_HOME=$javaHome)" } else { Write-Warn "JAVA_HOME을 찾을 수 없음 — 수동 설정 필요" } Write-OK "필수 패키지 설치 완료" # ── 2. Tomcat 9 설치 ──────────────────────────────────────── Write-Host "" Write-Host "[3/10] Tomcat 9 설치..." $TomcatVer = if ($env:TOMCAT_VER) { $env:TOMCAT_VER } else { "9.0.98" } $TomcatHome = "C:\app\tomcat" $TomcatMirror = if ($env:TOMCAT_MIRROR) { $env:TOMCAT_MIRROR } else { "https://archive.apache.org/dist/tomcat/tomcat-9/v$TomcatVer/bin" } if (-not (Test-Path "$TomcatHome\bin\startup.bat")) { $tarName = "apache-tomcat-$TomcatVer.zip" $tarUrl = "$TomcatMirror/$tarName" Write-Host " 다운로드: $tarUrl" try { Invoke-WebRequest $tarUrl -OutFile "$env:TEMP\$tarName" -TimeoutSec 120 -UseBasicParsing Expand-Archive "$env:TEMP\$tarName" -DestinationPath "C:\app" -Force Rename-Item "C:\app\apache-tomcat-$TomcatVer" $TomcatHome -ErrorAction SilentlyContinue if (-not (Test-Path $TomcatHome)) { Move-Item "C:\app\apache-tomcat-$TomcatVer" $TomcatHome } } catch { Write-Warn "Tomcat 다운로드 실패: $_" Write-Warn "TOMCAT_MIRROR 환경변수를 내부 미러로 설정하거나 수동 설치하세요." } } else { Write-Host " Tomcat이 이미 설치되어 있음: $TomcatHome" } # opsagent 계정 추가 (GUARDiA Manager 원격 제어용) $TomcatUsersXml = "$TomcatHome\conf\tomcat-users.xml" if ((Test-Path $TomcatUsersXml) -and -not (Select-String "opsagent" $TomcatUsersXml -Quiet)) { (Get-Content $TomcatUsersXml) -replace '', @" "@ | Set-Content $TomcatUsersXml -Encoding utf8 } # NSSM으로 Tomcat 9 Windows 서비스 등록 $tcSvc = "tomcat9" try { nssm stop $tcSvc 2>$null; nssm remove $tcSvc confirm 2>$null } catch {} nssm install $tcSvc "$TomcatHome\bin\tomcat9.exe" 2>$null if ($LASTEXITCODE -ne 0) { # tomcat9.exe 없으면 startup.bat 방식 nssm install $tcSvc "cmd" nssm set $tcSvc AppParameters "/c $TomcatHome\bin\startup.bat" } nssm set $tcSvc AppDirectory $TomcatHome nssm set $tcSvc AppEnvironmentExtra "JAVA_HOME=$env:JAVA_HOME CATALINA_HOME=$TomcatHome" nssm set $tcSvc Start SERVICE_AUTO_START nssm set $tcSvc AppStdout "C:\guardia\logs\tomcat9.log" New-Item -ItemType Directory -Force "C:\guardia\logs" | Out-Null nssm start $tcSvc Write-OK "Tomcat 9 서비스 등록 완료 (포트 8080)" # ── 2(b). Python 가상환경 ────────────────────────────────────── Write-Host "" Write-Host "[4/10] Python 가상환경 설정..." $venvPath = "C:\guardia\venv" if (-not (Test-Path $venvPath)) { python -m venv $venvPath } $pip = "$venvPath\Scripts\pip.exe" & $pip install --upgrade pip -q & $pip install -r "$GuardiaRoot\itsm\requirements.txt" -q Write-OK "Python 패키지 설치 완료" # ── 3. PostgreSQL 초기화 ──────────────────────────────────── Write-Host "" Write-Host "[4/8] PostgreSQL 초기화..." $pgBin = "C:\Program Files\PostgreSQL\16\bin" if (-not (Test-Path $pgBin)) { $pgBin = Get-ChildItem "C:\Program Files\PostgreSQL" -Directory | Sort-Object Name -Descending | Select-Object -First 1 | ForEach-Object { "$($_.FullName)\bin" } } $env:PGPASSWORD = "postgres" # DB/사용자 생성 & "$pgBin\psql.exe" -U postgres -c "CREATE USER guardia WITH PASSWORD 'guardia_secure_pw';" 2>$null & "$pgBin\psql.exe" -U postgres -c "CREATE DATABASE guardia OWNER guardia;" 2>$null Write-OK "PostgreSQL 설정 완료" # ── 4. Redis 서비스 등록 ──────────────────────────────────── Write-Host "" Write-Host "[5/10] Redis 서비스 등록..." $redisExe = (Get-Command redis-server -ErrorAction SilentlyContinue)?.Source if ($redisExe) { nssm install redis-server $redisExe 2>$null nssm start redis-server 2>$null } Write-OK "Redis 완료" # ── 5. 환경 설정 파일 ─────────────────────────────────────── Write-Host "" Write-Host "[6/10] 환경 설정 파일 생성..." $envFile = "$GuardiaRoot\itsm\.env" if (-not (Test-Path $envFile)) { @" DATABASE_URL=postgresql+asyncpg://guardia:guardia_secure_pw@localhost:5432/guardia SECRET_KEY=change_this_secret_key_in_production_min_32chars ALGORITHM=HS256 ACCESS_TOKEN_EXPIRE_MINUTES=480 REDIS_URL=redis://localhost:6379/0 OLLAMA_BASE_URL=http://localhost:11434 GUARDIA_LLM_MODEL=llama3.1:8b MESSENGER_BASE_URL=http://localhost:8002 MESSENGER_OPS_ROOM=ops "@ | Out-File -FilePath $envFile -Encoding utf8 Write-Warn ".env 생성됨 — SECRET_KEY를 변경하세요: $envFile" } # ── 6. DB 초기화 (스키마 불일치 자동 감지·복구) ──────────────────────── Write-Host "" Write-Host "[7/10] DB 초기화..." # 포트 8001 기존 프로세스 종료 (업그레이드 시 충돌 방지) $portProc = Get-NetTCPConnection -LocalPort 8001 -State Listen -ErrorAction SilentlyContinue | Select-Object -First 1 if ($portProc) { Write-Warn "포트 8001 사용 중 (PID $($portProc.OwningProcess)) — 종료..." Stop-Process -Id $portProc.OwningProcess -Force -ErrorAction SilentlyContinue Start-Sleep -Seconds 2 } Push-Location "$GuardiaRoot\itsm" $env:PYTHONIOENCODING = "utf-8" $env:PYTHONUNBUFFERED = "1" $dbResult = & "$venvPath\Scripts\python.exe" tools\db_init.py --force 2>&1 $dbResult | ForEach-Object { Write-Host " $_" } if ($LASTEXITCODE -ne 0) { Write-Fail "DB 초기화 실패 — 로그를 확인하세요" } Pop-Location Write-OK "DB 초기화 완료" # ── 7. NSSM Windows 서비스 등록 ───────────────────────────── Write-Host "" Write-Host "[8/10] Windows 서비스 등록 (NSSM)..." $uvicorn = "$venvPath\Scripts\uvicorn.exe" $svcName = "guardia-itsm" # 기존 서비스 중지 및 제거 (업그레이드) try { nssm stop $svcName 2>$null; Start-Sleep -Seconds 2 } catch {} try { nssm remove $svcName confirm 2>$null } catch {} nssm install $svcName $uvicorn nssm set $svcName AppParameters "main:app --host 0.0.0.0 --port 8001 --workers 4" nssm set $svcName AppDirectory "$GuardiaRoot\itsm" # PYTHONIOENCODING=utf-8 필수 — Windows 기본 cp949에서 한글 print 오류 방지 nssm set $svcName AppEnvironmentExtra "PATH=$venvPath\Scripts;$env:PATH PYTHONIOENCODING=utf-8 PYTHONUNBUFFERED=1" nssm set $svcName Start SERVICE_AUTO_START nssm set $svcName AppStdout "C:\guardia\logs\guardia-itsm.log" nssm set $svcName AppStderr "C:\guardia\logs\guardia-itsm-err.log" New-Item -ItemType Directory -Force "C:\guardia\logs" | Out-Null nssm start $svcName Write-OK "Windows 서비스 등록 완료" # ── Nginx 리버스 프록시 ────────────────────────────────────── $nginxConf = "C:\tools\nginx-winssl\conf\conf.d\guardia.conf" New-Item -ItemType Directory -Force (Split-Path $nginxConf) | Out-Null @" server { listen 80; server_name _; client_max_body_size 100M; location / { proxy_pass http://127.0.0.1:8001; proxy_http_version 1.1; proxy_set_header Upgrade `$http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host `$host; proxy_set_header X-Real-IP `$remote_addr; proxy_read_timeout 300s; } } "@ | Out-File -FilePath $nginxConf -Encoding utf8 # ── Ollama (온프레미스 sLLM) ────────────────────────────────── Write-Host "" Write-Host "[9/10] Ollama (온프레미스 sLLM 서버) 설치..." $OllamaInstall = if ($env:OLLAMA_INSTALL) { $env:OLLAMA_INSTALL } else { "online" } $OllamaModels = if ($env:OLLAMA_MODELS) { $env:OLLAMA_MODELS } else { "llama3.1:8b" } if ($OllamaInstall -eq "skip") { Write-Warn "Ollama 설치 건너뜀 (OLLAMA_INSTALL=skip) — 수동 설치 필요" } else { $ollamaExe = "$env:LocalAppData\Programs\Ollama\ollama.exe" if ($OllamaInstall -eq "offline") { $binPath = if ($env:OLLAMA_BIN_PATH) { $env:OLLAMA_BIN_PATH } else { "$env:TEMP\ollama-setup.exe" } if (Test-Path $binPath) { & $binPath /S 2>$null } else { Write-Warn "오프라인: OLLAMA_BIN_PATH에 설치파일 필요" } } else { try { choco install ollama -y --no-progress 2>&1 | Out-Null if (-not (Get-Command ollama -ErrorAction SilentlyContinue)) { # Chocolatey 실패 시 공식 설치파일 다운로드 $ollamaSetup = "$env:TEMP\OllamaSetup.exe" Invoke-WebRequest "https://ollama.com/download/OllamaSetup.exe" ` -OutFile $ollamaSetup -UseBasicParsing -TimeoutSec 120 & $ollamaSetup /S Start-Sleep -Seconds 5 } Write-OK "Ollama 설치 완료" } catch { Write-Warn "Ollama 설치 실패: $_ — 수동 설치: https://ollama.com/download" $OllamaInstall = "failed" } } if ($OllamaInstall -ne "failed") { # NSSM 서비스로 등록 (또는 기본 설치된 서비스 사용) $env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User") Start-Sleep -Seconds 3 # 모델 다운로드 (온라인 환경) if ($OllamaInstall -eq "online" -and (Get-Command ollama -ErrorAction SilentlyContinue)) { foreach ($model in $OllamaModels.Split(" ")) { Write-Host " 모델 다운로드: $model (시간이 걸릴 수 있습니다...)" ollama pull $model 2>&1 | Select-Object -Last 3 | ForEach-Object { Write-Host " $_" } } } Write-OK "Ollama 준비 완료 (http://localhost:11434)" } } # ── 방화벽 규칙 ────────────────────────────────────────────── New-NetFirewallRule -DisplayName "GUARDiA HTTP" -Direction Inbound -Protocol TCP -LocalPort 80 -Action Allow 2>$null New-NetFirewallRule -DisplayName "GUARDiA HTTPS" -Direction Inbound -Protocol TCP -LocalPort 443 -Action Allow 2>$null # Ollama/Tomcat은 내부 전용 — 외부 노출 차단 New-NetFirewallRule -DisplayName "Block Ollama External" -Direction Inbound -Protocol TCP -LocalPort 11434 -Action Block -RemoteAddress 0.0.0.0/0 2>$null New-NetFirewallRule -DisplayName "Block Tomcat External" -Direction Inbound -Protocol TCP -LocalPort 8080 -Action Block -RemoteAddress 0.0.0.0/0 2>$null Write-OK "방화벽 규칙 추가 완료 (80/443 허용, 8080/11434 내부 전용)" Write-Host "" Write-Host "==================================================" Write-OK "GUARDiA ITSM 설치 완료 — Windows Server [10/10]" Write-Host "" $serverIp = (Get-NetIPAddress -AddressFamily IPv4 | Where-Object { $_.InterfaceAlias -notlike "*Loopback*" } | Select-Object -First 1).IPAddress Write-Info "GUARDiA URL: http://$serverIp" Write-Info "Tomcat URL: http://${serverIp}:8080 (내부 전용)" Write-Info "Ollama URL: http://localhost:11434 (내부 전용)" Write-Info "설치 로그: $LogFile" Write-Info "검증: .\setup_windows.ps1 -Test" Write-Host "" Write-Warn "보안 필수 조치:" Write-Warn " 1. $envFile 의 SECRET_KEY 변경" Write-Warn " 2. PostgreSQL postgres 계정 비밀번호 변경" Write-Warn " 3. Tomcat opsagent 비밀번호 변경" Write-Host "==================================================" Stop-Transcript | Out-Null