포스트

IIS 블루그린 자동배포 — SSH로 스크립트 한 줄 배포

IIS 블루그린 자동배포 — SSH로 스크립트 한 줄 배포

이전 글에서 “나중에 해볼 일”로 남겨뒀던 IIS 자동배포를 실제로 구현했다.

로컬에서 ./deploy.sh 한 줄이면 빌드 → 전송 → IIS 경로 전환 → 재시작까지 자동으로 끝난다.

구조

1
2
3
4
5
6
7
로컬 PC                          Windows 서버
─────────                        ──────────────
dotnet publish                   
    ↓                           
SCP 전송  ──────────────────→   D:\apps\{날짜-번호}\
    ↓                               ↓
SSH 원격 PowerShell ────────→   IIS 경로 전환 + 재시작

이전 버전 폴더는 그대로 남아서 롤백도 경로만 바꾸면 된다.

Phase 1: 서버에 OpenSSH 설치

Windows Server에 OpenSSH를 설치하고, 회사 IP만 접근 가능하도록 방화벽을 설정한다.

1
2
3
4
5
6
7
8
9
10
11
# OpenSSH 서버 설치
Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0

# 서비스 시작 + 자동 시작
Start-Service sshd
Set-Service -Name sshd -StartupType Automatic

# 방화벽: 회사 IP + 내부 네트워크만 허용
New-NetFirewallRule -Name "SSH-Company" -DisplayName "SSH (회사만)" `
  -Direction Inbound -Protocol TCP -LocalPort 22 `
  -RemoteAddress "회사공인IP","192.168.0.0/24" -Action Allow

내부 대역(192.168.0.0/24) 필수. 같은 네트워크에서 접속 시 출발 IP가 192.168.0.x이므로 이것도 허용해야 한다. 안 하면 내부에서 Connection timed out.

외부에서도 접속하려면 공유기에 포트 포워딩을 추가한다. 22가 아닌 22022 같은 비표준 포트를 쓰면 봇 스캔을 피할 수 있다.

Phase 2: SSH 키 인증

비밀번호 없이 자동 접속하려면 SSH 키를 등록한다.

1
2
3
# 로컬에서 키 생성
ssh-keygen -t ed25519 -C "deploy@mycompany"
cat ~/.ssh/id_ed25519.pub

서버에서 관리자 PowerShell로:

1
2
3
4
5
6
7
# 공개 키 등록
Add-Content -Path "C:\ProgramData\ssh\administrators_authorized_keys" `
  -Value "ssh-ed25519 AAAA...공개키"

# 권한 설정 (이거 안 하면 키 인증이 무시됨)
icacls "C:\ProgramData\ssh\administrators_authorized_keys" `
  /inheritance:r /grant "SYSTEM:(F)" /grant "Administrators:(F)"

Phase 3: 배포 스크립트

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#!/bin/bash
SERVER="서버IP"
PORT=22
USER="deploy-user"
APP_PATH="D:/apps/myapp"
IIS_SITE="My_WebSite"
IIS_APP="myapp"
DATE=$(date +%Y-%m-%d)

# 1. 빌드
dotnet publish MyProject/MyProject.csproj -c Release -o ./publish --self-contained
rm -f ./publish/appsettings.json ./publish/appsettings.*.json

# 2. 전송
scp -P ${PORT} -r ./publish ${USER}@${SERVER}:"${APP_PATH}/"

# 3. 날짜-번호 폴더로 이름 변경
RELEASE_NAME=$(ssh -p ${PORT} ${USER}@${SERVER} "powershell -Command \"
  \$existing = (Get-ChildItem '${APP_PATH}' -Directory | Where-Object { \$_.Name -like '${DATE}*' }).Count;
  \$name = '${DATE}-' + (\$existing + 1);
  Rename-Item '${APP_PATH}/publish' \$name;
  Write-Output \$name\"")

# 4. 서버 설정 파일 복사
ssh -p ${PORT} ${USER}@${SERVER} "
  copy \"${APP_PATH//\//\\}\\appsettings.json\" \"${APP_PATH//\//\\}\\${RELEASE_NAME}\\\"
  copy \"${APP_PATH//\//\\}\\jwt-private.pem\" \"${APP_PATH//\//\\}\\${RELEASE_NAME}\\\""

# 5. IIS 경로 전환 + 재시작
ssh -p ${PORT} ${USER}@${SERVER} "powershell -Command \"
  Import-Module WebAdministration;
  Set-ItemProperty 'IIS:\Sites\\${IIS_SITE}\\${IIS_APP}' -Name physicalPath -Value '${APP_PATH//\//\\}\\${RELEASE_NAME}';
  Restart-WebItem 'IIS:\Sites\\${IIS_SITE}';
  Restart-WebAppPool '${IIS_SITE}.${IIS_APP}'\"" 

# 6. 정리
rm -rf ./publish
echo "배포 완료: ${APP_PATH}/${RELEASE_NAME}"

주의사항

--self-contained 필수. --no-self-contained로 빌드하면 서버에 런타임이 없을 수 있어 500.31 에러가 난다.

appsettings.json은 서버별로 관리. 로컬 빌드의 appsettings.json을 서버에 올리면 안 된다. 스크립트에서 빌드 후 제거하고, 서버에 있는 설정 파일을 복사한다.

앱풀 재시작 필요. 웹사이트 재시작만으로는 부족하다. Restart-WebAppPool도 해야 확실히 새 코드가 로드된다.

롤백

이전 버전 폴더가 남아있으므로 경로만 바꾸면 된다.

1
2
3
Import-Module WebAdministration
Set-ItemProperty "IIS:\Sites\사이트명\앱이름" -Name physicalPath -Value "D:\apps\myapp\이전날짜-번호"
Restart-WebItem "IIS:\Sites\사이트명"

Phase 4: 무중단 배포

위 Phase 3까지는 Restart-WebItem(웹사이트 재시작)을 쓰는데, 이러면 모든 연결이 즉시 끊긴다. 무중단 배포는 Restart-WebAppPool(앱풀 재활용)만 써서 새 프로세스가 먼저 뜨고, 기존 요청은 기존 프로세스가 마저 처리 후 종료하는 방식이다.

IIS 설정 (서버에서 1회, 앱풀마다)

1
2
3
4
5
6
7
8
9
10
11
12
13
Import-Module WebAdministration

# 1. Application Initialization 기능 활성화 (서버 전체 1회)
Enable-WindowsOptionalFeature -Online -FeatureName IIS-ApplicationInit -NoRestart

# 2. 앱풀: AlwaysRunning (항상 실행 상태 유지)
Set-ItemProperty "IIS:\AppPools\앱풀이름" -Name startMode -Value AlwaysRunning

# 3. 앱: preloadEnabled (앱풀 재활용 시 새 프로세스 미리 시작)
Set-ItemProperty "IIS:\Sites\사이트명\앱이름" -Name preloadEnabled -Value True

# 4. Overlapped Rotation 확인 (False = 허용, 기본값)
(Get-ItemProperty "IIS:\AppPools\앱풀이름" -Name recycling.disallowOverlappingRotation).Value
설정의미
startModeAlwaysRunning앱풀이 항상 실행
preloadEnabledTrue재활용 시 새 프로세스 미리 시작
disallowOverlappingRotationFalse기존+새 프로세스 동시 존재 허용

배포 스크립트 변경점

  • Restart-WebItem (웹사이트 재시작) 제거 — 모든 연결을 끊으므로
  • Restart-WebAppPool만 사용 — 무중단 재활용

이 세 가지 설정이 맞물리면, 배포 시 사용자는 끊김 없이 서비스를 이용할 수 있다.

아직 실제 트래픽 환경에서 무중단 테스트는 하지 않았다. 운영 서버에 적용할 때 부하 테스트와 함께 검증할 예정.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.