왜 이걸 하게 됐나
Unity로 사이드 프로젝트를 만들다 보면 Netcode, Mirror 같은 엔진·패키지에 기대서 멀티플레이를 붙일 때가 많다. 나도 그랬다. 그런데 어느 순간부터 “원리는 얼마나 이해하고 있을까?”라는 질문이 생겼다. 그래서 이번에는 .NET과 SignalR로 직접 매치메이킹 서버를 만들고, 그걸 AWS 프리 티어 EC2에 올려 운영해 보기로 했다.
목표는 단순했다.
1. 코드가 바뀌면 자동으로 배포될 것(CI/CD)
2. 최소비용으로 안정적으로 돌아갈 것
아키텍처 한 줄 요약
- 서버: .NET 8 + ASP.NET Core + SignalR (
/matchmakingHub) - 런타임: EC2 t3.micro (Ubuntu 22.04)
- 프록시: Nginx(80/443) → Kestrel(127.0.0.1:5000)
- 운영: systemd 서비스, GitHub Actions로 CI/CD
첫 번째 벽: .NET SDK 버전 충돌
리포지토리에 global.json이 있어 로컬 SDK와 버전이 맞지 않았다. 나는 “설치” 대신 롤포워드를 택했다.
cd "My project"
cat > global.json << 'JSON'
{
"sdk": {
"version": "8.0.412",
"rollForward": "latestMajor"
}
}
JSON
빌드와 배포
t3.micro는 x86_64라서 linux-x64로 퍼블리시했다.
cd "My project/MatchmakingServer"
dotnet restore
dotnet publish -c Release -r linux-x64 --no-self-contained -o publish
EC2로 파일을 보냈다.
scp -i ~/.ssh/your-key.pem -r "./publish" ubuntu@EC2_PUBLIC_IP:/home/ubuntu/matchmaking
Kestrel을 서비스로 띄우기
처음엔 www-data로 돌렸다가 /home/ubuntu 경로 접근 권한 때문에 systemd가 200/CHDIR로 죽었다. 운영 계정을 ubuntu로 바꾸니 바로 살아났다.
sudo tee /etc/systemd/system/matchmaking.service >/dev/null <<'EOF'
[Unit]
Description=Matchmaking Server
After=network.target
[Service]
WorkingDirectory=/home/ubuntu/matchmaking/publish
ExecStart=/usr/bin/dotnet /home/ubuntu/matchmaking/publish/MatchmakingServer.dll
Restart=always
Environment=ASPNETCORE_ENVIRONMENT=Production
Environment=ASPNETCORE_URLS=http://127.0.0.1:5000
User=ubuntu
Group=ubuntu
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now matchmaking
sudo systemctl status matchmaking | cat
Nginx 프록시로 외부 트래픽 받기
기본 페이지가 떠서 당황했지만, 결국은 디폴트 사이트가 우선 응답하고 있었던 것. 프록시 서버 블록을 기본 서버로 등록하고 디폴트를 껐다.
sudo tee /etc/nginx/sites-available/matchmaking >/dev/null <<'EOF'
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 80 default_server;
server_name _;
location / {
proxy_pass http://127.0.0.1:5000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
proxy_read_timeout 3600s;
}
}
EOF
sudo rm -f /etc/nginx/sites-enabled/default
sudo ln -sf /etc/nginx/sites-available/matchmaking /etc/nginx/sites-enabled/matchmaking
sudo nginx -t
sudo systemctl reload nginx
테스트 클라이언트의 함정
wwwroot/index.html의 SignalR URL이 http://localhost:5000으로 하드코딩되어 있었다. 상대경로로 바꾸니 프록시 뒤에서도 잘 붙었다.
sudo sed -i 's|http://localhost:5000/matchmakingHub|/matchmakingHub|g' /home/ubuntu/matchmaking/publish/wwwroot/index.html
동작 확인:
curl -i http://127.0.0.1:5000/
curl -i http://127.0.0.1:5000/index.html
curl -i http://EC2_PUBLIC_IP/
curl -i http://EC2_PUBLIC_IP/index.html
브라우저에서 접속해 Connect → Start Matchmaking까지 확인했고, Unity 클라이언트에서도 http://EC2_PUBLIC_IP/matchmakingHub로 연결이 되었다.
자동 배포(CI/CD)를 붙이기
수동 rsync는 금방 한계를 드러낸다. 작은 수정에도 서버에 들어가 재시작해야 하니까. 그래서 GitHub Actions로 빌드→EC2 동기화→서비스 재시작까지 자동화했다.
사전 준비:
- EC2에서
systemctl무비번
echo 'ubuntu ALL=(ALL) NOPASSWD: /bin/systemctl' | sudo tee /etc/sudoers.d/ubuntu-systemctl
- 배포용 SSH 키 생성 후 공개키 등록
ssh-keygen -t ed25519 -f ~/.ssh/ec2_github -C github-actions
cat ~/.ssh/ec2_github.pub >> ~/.ssh/authorized_keys
chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keys
- GitHub Secrets
EC2_HOST: 예) 13.211.154.30EC2_USER: ubuntuEC2_PATH: /home/ubuntu/matchmakingEC2_SSH_KEY: 배포용 개인키 전체 내용
워크플로 파일은 리포지토리 루트의 .github/workflows/deploy-matchmaking.yml에 두었다. main과 MCP 브랜치에 푸시되면 자동으로 동작한다.
name: Deploy MatchmakingServer
on:
push:
branches: [ main, MCP ]
paths:
- 'My project/MatchmakingServer/**'
- 'My project/SharedContracts/**'
- '.github/workflows/deploy-matchmaking.yml'
workflow_dispatch:
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
global-json-file: 'My project/global.json'
- name: Publish
working-directory: "My project/MatchmakingServer"
run: |
dotnet restore
dotnet publish -c Release -r linux-x64 --no-self-contained -o publish
- name: Prepare SSH
run: |
mkdir -p ~/.ssh
echo "${{ secrets.EC2_SSH_KEY }}" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
ssh-keyscan -H ${{ secrets.EC2_HOST }} >> ~/.ssh/known_hosts
- name: Rsync to EC2
working-directory: "My project/MatchmakingServer"
run: |
rsync -az --delete -e "ssh -i ~/.ssh/id_ed25519 -o StrictHostKeyChecking=no" publish/ ${{ secrets.EC2_USER }}@${{ secrets.EC2_HOST }}:${{ secrets.EC2_PATH }}/
- name: Post-deploy
run: |
ssh -i ~/.ssh/id_ed25519 -o StrictHostKeyChecking=no ${{ secrets.EC2_USER }}@${{ secrets.EC2_HOST }} "\
sudo sed -i 's|http://localhost:5000/matchmakingHub|/matchmakingHub|g' ${{ secrets.EC2_PATH }}/wwwroot/index.html || true && \
sudo systemctl restart matchmaking && \
sleep 2 && curl -sf http://127.0.0.1:5000/ | head -c 200"
Actions 탭에서 녹색 체크를 보자마자, 브라우저 새로고침으로 배포 결과를 확인할 수 있었다. 커밋이 배포가 되는 경험은 역시 중독적이다.
내가 얻은 것
- 원리 이해: Kestrel은 내부 포트에서 앱을 띄우고, Nginx가 외부 트래픽을 받아 프록시한다. process manager(systemd)가 앱을 유지한다.
- 문제 해결 감각: 502는 대개 앱이 안 떠있거나 경로/권한 문제다.
status=200/CHDIR는 작업 디렉터리 접근 권한 힌트다. - 빌드 체인: SDK 고정(
global.json)과 롤포워드, 타겟 런타임(linux-x64)의 의미가 명확해졌다. - 운영 감각: SSH 키,
authorized_keys, 디렉터리 권한, 헬스 체크, 롤백 흐름이 손에 익었다.
다음
HTTPS 자동화, 로그·메트릭 수집(모니터링), 그리고 멀티 인스턴스 확장을 위한 Redis 백플레인을 고민 중이다.
'Unity' 카테고리의 다른 글
| [Unity] DOTS 개념 정리: Struct 구조 변경, ECB 사용법 등 (0) | 2025.04.23 |
|---|---|
| [Unity] DOTS 실전 개념 정리 - Tag, EnableableComponent, CompanionLink 이해하기 (0) | 2025.04.23 |
| [Unity] DOTS 성능 최적화 단계별 비교 (0) | 2025.04.13 |
| [Unity] DOTS 이해하기 (0) | 2025.04.08 |
| [Unity] NetCode 공부일지 (0) | 2025.04.05 |