Agones와 Kubernetes(K8s)를 활용한 전용 게임 서버 오케스트레이션 및 오토스케일링 가이드
상태를 유지하는 전용 게임 서버(DGS)를 Kubernetes 환경에서 안정적으로 배포·관리하기 위한 Agones의 핵심 아키텍처, UDP 포트 할당, SDK 라이프사이클 연동 및 버퍼 기반 FleetAutoscaler 구성 전략을 다룹니다.

핵심 요약
일반적인 웹 백엔드(Stateless)와 달리 실시간 전용 게임 서버(Dedicated Game Server, DGS)는 클라이언트의 실시간 인메모리 상태를 유지하는 Stateful 워크로드입니다. 일반 Kubernetes의 Pod 수명 주기와 로드밸런서를 그대로 적용하면 게임 도중 서버가 강제 종료되거나 UDP 트래픽 라우팅 오버헤드가 발생합니다. 오픈소스 오케스트레이터인 Agones는 Kubernetes의 CRD(Custom Resource Definition)와 gRPC 기반 SDK를 통해 게임 서버의 라이프사이클 제어, 동적 UDP 포트 바인딩 그리고 활성 세션을 보호하는 버퍼 기반 오토스케일링(FleetAutoscaler)을 구현합니다.
왜 일반 Kubernetes로는 전용 게임 서버(DGS)를 운영하기 어려울까?
Kubernetes는 본래 무상태(Stateless) 웹 애플리케이션이나 마이크로서비스를 효율적으로 운영하기 위해 설계되었습니다. 반면 멀티플레이 액션/FPS/MOBA 게임의 전용 게임 서버(DGS)는 전혀 다른 인프라적 요구사항을 가집니다.
| 비교 항목 | 웹 애플리케이션 (Web/API) | 전용 게임 서버 (Dedicated Game Server) |
|---|---|---|
| 상태 유지 여부 | Stateless (외부 캐시/DB에 저장) | Stateful (프로세스 메모리에 월드/물리 상태 유지) |
| 네트워크 프로토콜 | HTTP/1.1, HTTP/2, gRPC (TCP 중심) | 실시간 저지연 패킷 통신 (UDP 중심) |
| 로드밸런싱 방식 | L4/L7 로드밸런서가 요청 단위 분산 | 매치메이커가 특정 DGS 인스턴스에 플레이어 직접 연결 |
| 스케일 인 (Scale-in) | 트래픽 감소 시 임의의 Pod 즉시 종료 가능 | 게임이 진행 중인(Allocated) Pod는 절대 종료 금지 |
| 연결 지속 시간 | 수 밀리초 ~ 수 초 단위의 짧은 요청 | 매치 시작부터 종료까지 10분~60분 이상 연속 연결 |
일반적인 Kubernetes Deployment를 사용하여 게임 서버를 스케일 인하면 Kubernetes 컨트롤러는 임의의 Pod를 선택하여 SIGTERM을 전송하고 삭제합니다. 이 과정에서 한창 매치를 진행 중이던 플레이어들의 세션이 강제로 끊어지는 치명적인 사고가 발생합니다.
또한 Kubernetes의 기본 Service(ClusterIP, NodePort)는 L4 로드밸런싱 계층을 거치므로 UDP 패킷 처리에 불필요한 홉(Hop)과 지연 시간(Latency)을 추가하며 클라이언트가 특정 게임 룸 인스턴스의 IP와 포트로 직접 패킷을 전송하는 구조와 맞지 않습니다.

Agones의 핵심 아키텍처와 커스텀 리소스(CRD)
Agones는 Kubernetes 위에 설치되는 오픈소스 플랫폼으로 게임 서버 전용 커스텀 리소스(CRD)와 오퍼레이터(Operator) 그리고 사이드카 SDK를 제공하여 DGS의 생명주기를 완벽하게 제어합니다.
1. 주요 커스텀 리소스 구성
GameServer: 단일 게임 서버 프로세스를 감싸는 Pod 단위의 리소스입니다. 고유한 컨테이너 포트와 노드의 물리 포트를 매핑하고 수명 주기를 추적합니다.Fleet: 일반 Kubernetes의Deployment에 대응하는 리소스입니다. 동일한 스펙을 가진GameServer집합을 관리하며 무중단 롤링 업데이트 및 안전한 스케일링을 지원합니다.GameServerAllocation: 매치메이킹 시스템이 플레이 가능한 서버를 요청할 때 사용하는 원자적(Atomic) 할당 리소스입니다. 경쟁 상태(Race Condition) 없이 유휴 상태(Ready)인 서버를 점유 상태(Allocated)로 전환합니다.FleetAutoscaler:Fleet의 상태를 모니터링하며 현재 할당된 서버 수에 맞춰 즉시 투입 가능한 여유 서버(Ready버퍼)를 자동으로 확보합니다.
2. GameServer의 상태 전이(State Machine)
Agones는 게임 서버의 상태를 엄밀하게 분리하여 관리합니다.
stateDiagram-v2
[*] --> PortAllocation: Pod 생성 요청
PortAllocation --> Creating: Node 및 HostPort 바인딩
Creating --> Starting: Agones SDK 초기화 대기
Starting --> Ready: SDK.Ready() 호출
Ready --> Allocated: GameServerAllocation (매치 할당)
Allocated --> Shutdown: SDK.Shutdown() (매치 종료)
Ready --> Unhealthy: SDK.Health() 핑 실패
Allocated --> Unhealthy: SDK.Health() 핑 실패
Unhealthy --> Shutdown: 비정상 Pod 자동 축출
Shutdown --> [*]: 리소스 회수
PortAllocation/Creating: 노드의 포트 풀에서 사용 가능한 UDP 포트를 할당받고 컨테이너를 구동합니다.Starting: 바이너리가 로드되고 맵 및 리소스 에셋을 메모리에 올리는 단계입니다.Ready: 준비가 완료되어 매치메이커의 할당을 대기하는 유휴 상태입니다.Allocated: 플레이어들이 입장하여 실제 게임이 진행 중인 상태입니다. 이 상태의 서버는 스케일 인 정책에 의해 절대 임의로 종료되지 않습니다.Unhealthy: 서버 프로세스가 크래시되거나 데드락에 걸려 헬스체크 핑이 누락된 상태입니다. Agones 오퍼레이터가 이를 감지하고 자동으로 재시작합니다.
Agones SDK 연동 및 게임 서버 컨테이너화 Step-by-Step
게임 서버 바이너리가 Agones와 통신하려면 Agones SDK(C++, C#, Go, Rust, Node.js 등 지원)를 코드에 통합해야 합니다.
1단계: 게임 서버 코드에 Agones SDK 연동 (C++ 예시)
게임 서버 프로세스는 내부 사이드카(Sidecar)와 로컬 gRPC 통신을 통해 자신의 상태를 알립니다.
#include <agones/sdk.h>
#include <atomic>
#include <thread>
#include <chrono>
#include <iostream>
class GameServerManager {
private:
agones::SDK* sdk;
std::atomic<bool> running;
std::thread health_thread;
public:
GameServerManager() : running(false) {
sdk = new agones::SDK();
}
bool Initialize() {
// 1. Agones SDK 사이드카와 gRPC 연결 초기화
if (!sdk->Connect()) {
std::cerr << "Agones SDK 연결 실패!" << std::endl;
return false;
}
// 2. 백그라운드 헬스체크 핑 스레드 시작 (2초 주기)
running = true;
health_thread = std::thread([this]() {
while (running.load()) {
sdk->Health();
std::this_thread::sleep_for(std::chrono::seconds(2));
}
});
// 3. 에셋 로딩 완료 후 Ready 상태 전송
sdk->Ready();
std::cout << "GameServer가 Ready 상태로 전이되었습니다." << std::endl;
return true;
}
void OnMatchEnded() {
std::cout << "게임 매치 종료. Shutdown을 요청합니다." << std::endl;
StopHealthThread();
// 4. 게임 종료 후 Pod 정상 삭제 요청
sdk->Shutdown();
}
~GameServerManager() {
StopHealthThread();
delete sdk;
}
private:
void StopHealthThread() {
running.store(false);
if (health_thread.joinable()) {
health_thread.join();
}
}
};
2단계: GameServer 및 Fleet 매니페스트 작성
동적 포트 할당(Dynamic)을 설정하여 노드의 포트 충돌 없이 여러 개의 게임 서버 컨테이너를 단일 노드에 밀도 높게 패킹합니다.
apiVersion: "agones.dev/v1"
kind: Fleet
metadata:
name: dedicated-fps-server
spec:
replicas: 10
template:
spec:
ports:
- name: default
containerPort: 7777
portPolicy: Dynamic
protocol: UDP
health:
initialDelaySeconds: 15
periodSeconds: 3
failureThreshold: 3
template:
spec:
containers:
- name: dedicated-server
image: gcr.io/my-game-project/fps-server:v1.2.0
resources:
requests:
cpu: "1000m"
memory: "2Gi"
limits:
cpu: "2000m"
memory: "4Gi"
3단계: 매치메이커의 게임 서버 할당(GameServerAllocation)
매치메이킹이 완료되면 매치메이커 백엔드는 Kubernetes API를 통해 GameServerAllocation을 요청하여 안전하게 서버 1대를 획득합니다.
apiVersion: "allocation.agones.dev/v1"
kind: GameServerAllocation
metadata:
generateName: allocation-
spec:
selectors:
- matchLabels:
agones.dev/fleet: dedicated-fps-server
gameServerState: Ready
API 응답으로 반환된 status.address(노드 공인 IP)와 status.ports[0].port(동적 할당된 외부 UDP 포트)를 클라이언트들에게 전달하면 클라이언트는 방화벽이나 프록시 지연 없이 해당 DGS에 직결(Direct Connect)됩니다.

버퍼 기반 FleetAutoscaler와 인프라 스케일링 전략
게임 트래픽은 접속자 수의 급격한 유입과 유출(Spike)이 빈번합니다. Agones는 즉각적인 매치메이킹 지연을 방지하기 위해 버퍼(Buffer) 기반 오토스케일링을 사용합니다.
1. 버퍼 기반 오토스케일링 원리
현재 할당된(Allocated) 서버 수 외에 신규 플레이어가 대기 시간 없이 즉시 진입할 수 있는 유휴(Ready) 서버 버퍼를 유지합니다.
고정 크기 버퍼 정책에서의 목표 레플리카 수식:
비율(백분율) 기반 버퍼 정책 수식 ():
2. FleetAutoscaler 매니페스트 구성
apiVersion: "autoscaling.agones.dev/v1"
kind: FleetAutoscaler
metadata:
name: dedicated-fps-autoscaler
spec:
fleetName: dedicated-fps-server
policy:
type: Buffer
buffer:
bufferSize: 5
minReplicas: 10
maxReplicas: 200
3. 클러스터 노드 오토스케일러(Karpenter / Cluster Autoscaler) 연동 주의점
Kubernetes 노드가 축출(Drain)되거나 클러스터 오토스케일러가 노드를 회수할 때, Allocated 상태의 DGS가 실행 중인 노드가 종료되지 않도록 보호해야 합니다.
- Pod Safe-to-Evict 방지: Agones는
Allocated상태가 되면 자동으로 Pod 주석에cluster-autoscaler.kubernetes.io/safe-to-evict: "false"를 부여합니다. - Karpenter 연동 시 Do-Not-Disrupt: Karpenter v1 환경에서는
karpenter.sh/do-not-disrupt: "true"어노테이션을 설정하여 매치가 진행 중인 노드의 강제 통합(Consolidation)을 차단해야 합니다.
자주 묻는 질문 (FAQ)
Q1. 게임 진행 도중 노드가 스케일 인(축출)될 때 플레이 중인 세션이 종료되지 않나요?
Agones는 GameServerAllocation을 통해 서버가 Allocated 상태로 바뀌면 해당 Pod에 축출 방지 어노테이션을 자동으로 주입합니다. 따라서 Kubernetes의 노드 오토스케일러나 Fleet 스케일 인 컨트롤러는 Ready 상태의 여유 Pod만 삭제하며 게임이 진행 중인 Pod가 있는 노드는 세션이 끝날 때까지 삭제되지 않습니다.
Q2. 수천 개의 게임 서버가 뜰 때 UDP 포트 충돌은 어떻게 방지하나요?
Agones 컨트롤러는 클러스터 노드별로 사전 정의된 포트 범위(기본값: 7000~8000 또는 사용자 지정 범위)를 관리합니다. portPolicy: Dynamic을 설정하면 노드 레벨에서 현재 사용되지 않는 고유한 호스트 포트(HostPort)를 원자적으로 배정하므로 포트 충돌이 원천 차단됩니다.
Q3. Kubernetes Service나 Ingress 없이 클라이언트가 어떻게 서버에 접속하나요?
매치메이커가 GameServerAllocation API를 호출하면 응답 바디에 할당된 게임 서버의 노드 외부 공인 IP(status.address)와 할당된 노드 포트(status.ports[0].port)가 즉시 반환됩니다. 클라이언트는 이 IP/Port 정보를 받아 소켓 직접 연결(Direct UDP Connection)을 수행하므로 프록시 오버헤드가 발생하지 않습니다.
마치며: 프로덕션 운영을 위한 베스트 프랙티스
Agones를 실제 프로덕션 라이브 서비스에 적용할 때는 다음 항목을 필히 검토해야 합니다.
- 헬스체크 주기 최적화: 게임 로직 루프가 일시적인 랙(GC 멈춤, 맵 로딩)으로 헬스체크 핑을 놓치지 않도록
failureThreshold와periodSeconds를 넉넉하게 튜닝합니다. - 노드 테인트(Taint) 및 톨러레이션(Toleration): 게임 서버 전용 노드 풀(Node Pool)을 구성하고 테인트를 적용하여 시스템 데몬셋이나 무관한 Pod가 게임 서버 노드의 CPU/메모리를 잠식하지 않도록 분리합니다.
- 로그 및 코어 덤프 보존:
Shutdown된 Pod는 즉시 클러스터에서 제거되므로 Fluent Bit이나 로깅 에이전트를 통해 종료 직전의 표준 출력 로그와 비정상 종료 시의 코어 덤프를 오브젝트 스토리지(S3, GCS)로 자동 전송하도록 구성합니다.



