GCP Minecraft 서버 운영 구조
상태를 보존하는 Compute Engine VM 한 대에서 플레이어 접속, 부팅, 외부 복구, 백업과 Discord 운영 경로를 어떻게 나눴을까?
Windows PC의 월드와 서버 파일을 Compute Engine으로 옮긴 뒤 고정 외부 IP, Java·Bedrock 접속, 선점 복구, 원격 백업과 Discord 운영 기능을 붙였다. Purpur, Geyser, Discord 봇과 백업 작업은 월드를 보관한 VM 한 대에서 실행됐다. 선점으로 이 VM이 멈추면 VM 밖의 복구 제어면이 같은 인스턴스를 다시 시작했다.

GCP 밖의 플레이어와 Discord 요청은 VPC 경계를 지나 Compute Engine VM으로 들어온다. VM 안에서는 systemd가 Purpur와 Discord 봇을 시작·재시작하고 백업 timer를 실행한다. Geyser와 Floodgate는 별도 systemd 서비스가 아니라 Purpur가 플러그인으로 로드한다. 월드 백업은 GCP 관리형 Cloud Storage로 나가고, 자동 복구 제어면은 GCP 안에서도 VM과 분리되어 종료 활동 로그를 감지한 뒤 Compute API로 같은 VM의 시작을 요청한다. Terraform은 이 런타임 경로에 참여하지 않으므로 구조도에서는 제외했다.
| 경로 | 시작점 | 핵심 구성요소 | 완료 조건 | 대표 실패 |
|---|---|---|---|---|
| 접속 | Minecraft 클라이언트 | 외부 IP, 방화벽, Purpur, Geyser, Floodgate | Java·Bedrock 프로토콜 응답과 실제 접속 | bind 실패, TCP·UDP 규칙 누락 |
| 부팅 | Compute API | 운영체제, startup script, systemd, JVM | 서버 로그의 준비 완료와 외부 상태 응답 | VM만 RUNNING, 서비스 재시작 반복 |
| 복구 | 종료 활동 로그 | Logging, Monitoring, Pub/Sub, 복구 함수 | 같은 VM이 다시 Minecraft 요청에 응답 | 수동 중지를 선점으로 오인, 중복 실행 |
| 보존 | systemd timer | RCON, 작업 복사본, tar/pigz, Cloud Storage | 원격 보관본 생성과 격리된 복원 시험 | 실행 중 파일 변경, 압축 형식 불일치 |
네 경로가 향하는 곳은 같은 VM이지만 실행 위치까지 모두 같지는 않다. 접속과 부팅은 VM 안의 프로세스로 이어지고, 복구 실행기와 백업 목적지는 VM 밖에 있다. 특히 보존 경로의 완료 조건인 복원 시험은 운영 당시 확인하지 못했다.
Terraform이 만든 경계
Terraform은 게임 요청을 처리하지 않았다. 고정 외부 IP, 방화벽, VM, 버킷, 함수와 IAM을 선언하고 VM metadata를 통해 startup script와 설정을 전달했다. 선언된 리소스와 런타임 프로세스의 경계는 다음과 같다.
flowchart TD accTitle: Terraform 리소스와 런타임 의존 관계 accDescr: Terraform이 주소, 방화벽, VM, 버킷과 복구 리소스를 만들고 VM 부팅 뒤 systemd가 Minecraft와 Discord 봇을 실행한다. TF[Terraform] --> IP[고정 외부 IP] TF --> FW[Java TCP·Bedrock UDP 방화벽] TF --> VM[Compute Engine VM] TF --> GCS[백업 Cloud Storage] TF --> CTRL[Logging·Monitoring·Pub/Sub·함수] IP --> NIC[VM access_config] FW --> TAG[VM network tag] NIC --> VM TAG --> VM VM --> START[startup script] START --> SD[systemd] SD --> MC[Purpur] MC --> GEY[Geyser plugin] MC --> FLOOD[Floodgate plugin] SD --> BOT[Discord bot] SD --> BACKUP[backup timer] BACKUP --> GCS CTRL --> VM
고정 주소와 VM 연결은 access_config.nat_ip에서 일어난다. 방화벽은 주소에 붙는 것이 아니라
VM의 network tag를 대상으로 삼는다. 주소, 규칙, 태그 중 하나만 빠져도 외부 경로가
완성되지 않는다.
resource "google_compute_address" "minecraft" {
name = "minecraft-public-ip"
region = var.region
}
resource "google_compute_firewall" "minecraft_java" {
name = "minecraft-java"
network = var.network
source_ranges = ["0.0.0.0/0"]
target_tags = ["minecraft-server"]
allow {
protocol = "tcp"
ports = ["25565"]
}
}
Bedrock 접속은 같은 모양의 규칙에 protocol = "udp"와 Bedrock 포트를 사용한다. Java
규칙을 복사하면서 프로토콜을 TCP로 남겨 두면 VM 내부에서 Geyser가 정상이어도 외부
Bedrock 패킷은 도달하지 않는다. RCON과 SSH는 공개 게임 포트와 분리하고 관리자 대역 또는
내부 경로만 허용해야 한다.
상태를 보존하는 VM 실행 노드
월드와 플러그인 상태는 VM의 영구 디스크에 있었다. 복구 함수가 새 서버를 조립한 것이 아니라 종료된 같은 VM을 다시 시작한 이유다. 당시 Terraform은 레거시 선점형 옵션과 자동 재시작 비활성화를 함께 사용했다.
resource "google_compute_instance" "minecraft" {
name = "minecraft-server"
machine_type = var.machine_type
zone = var.zone
tags = ["minecraft-server"]
scheduling {
preemptible = true
automatic_restart = false
}
network_interface {
network = var.network
access_config {
nat_ip = google_compute_address.minecraft.address
}
}
}
현재 Spot VM 표현은 provisioning_model = "SPOT"이다. Spot VM에는 레거시 선점형 VM의
24시간 제한이 없지만 언제든 회수될 수 있고 SLA와 자동 재시작을 제공하지 않는다. 중지된
Spot VM도 용량이 있으면 다시 시작할 수 있으므로 당시의 외부 복구 원리는 여전히 적용할 수
있다. 종료 동작은 보존할 VM이면 STOP, 일회성 작업이면 DELETE로 의도를 분명히 한다.
scheduling {
provisioning_model = "SPOT"
instance_termination_action = "STOP"
automatic_restart = false
on_host_maintenance = "TERMINATE"
}
Minecraft처럼 로컬 디스크 상태와 고정 주소를 유지할 단일 서버에는 같은 VM 재시작이 단순하다. 무상태 프록시나 동일 이미지로 교체 가능한 작업이라면 Managed Instance Group이 인스턴스 재생성과 상태 관리를 맡게 하는 편이 낫다. 어느 쪽이든 월드를 부팅 디스크 하나에만 두면 삭제와 재생성의 의미가 커지므로 데이터 디스크와 백업 경계를 따로 정해야 한다.
RUNNING과 게임 준비 사이
Compute Engine 상태가 RUNNING으로 바뀐 뒤에도 운영체제, startup script, systemd와
JVM 초기화가 남는다. 당시 startup script는 패키지 설치, 서버 파일 복원, 서비스 파일
생성과 시작을 한 번에 수행했다. 외부 다운로드가 느리거나 압축 해제가 실패하면 VM 상태와
게임 상태가 갈라진다.
sequenceDiagram accTitle: VM 시작부터 Minecraft 접속 준비까지 accDescr: Compute API가 VM을 시작한 뒤 운영체제, startup script, systemd, JVM과 플러그인이 차례로 준비되고 외부 상태 조회가 마지막 완료 조건이 된다. participant API as Compute API participant VM as VM·OS participant Boot as startup script participant SD as systemd participant Java as Purpur JVM participant Probe as 외부 probe API->>VM: instances.start VM-->>API: RUNNING VM->>Boot: metadata startup script Boot->>SD: unit 설치·enable SD->>Java: ExecStart Java->>Java: 월드·플러그인 로드 Java-->>SD: 프로세스 실행 유지 Probe->>Java: Minecraft status Java-->>Probe: protocol 응답
systemd는 프로세스 생명주기를 맡는다. 서비스의 Restart=always는 JVM이 비정상 종료됐을
때 유용하지만 설정 오류로 즉시 종료하는 프로세스도 반복 실행한다. systemctl is-active,
Java PID와 포트 리스너만으로 준비 완료를 선언하지 않고 서버 로그의 준비 메시지와
Minecraft protocol 응답까지 확인해야 한다.
[Service]
WorkingDirectory=/opt/minecraft
ExecStart=/usr/bin/java -Xms8G -Xmx8G -jar purpur.jar nogui
Restart=always
RestartSec=10
실제 unit은 root로 실행됐다. 서버 전용 Unix 사용자와 그룹을 만들면 플러그인이나 RCON을 통해 문제가 생겼을 때 수정 가능한 파일 범위를 줄일 수 있다. 월드, 플러그인과 로그만 해당 사용자가 소유하고 systemd unit과 운영 스크립트는 root가 관리하는 구성이 경계를 더 잘 드러낸다.
외부에 둔 복구 제어면
VM이 선점되면 VM 내부의 Minecraft, Discord 봇, timer와 systemd가 모두 멈춘다. 따라서 종료를 관찰하고 VM을 시작하는 실행기는 VM 밖에 있어야 한다. 실제 경로는 Compute Engine 활동 로그를 로그 기반 메트릭으로 바꾸고 Monitoring 알림을 Pub/Sub으로 전달한 뒤 1세대 Cloud Function이 Compute API를 호출하는 형태였다.
flowchart LR accTitle: VM 선점 자동 복구 제어 경로 accDescr: 종료 로그를 감지하는 관측 계층, 이벤트를 전달하고 복구를 판단하는 제어 계층, VM과 Minecraft를 다시 올리는 실행 계층으로 나뉜다. DETECT[종료 로그 감지<br/>Logging metric·alert] --> CONTROL[이벤트 전달·복구 판단<br/>Pub/Sub·recovery function] CONTROL --> BOOT[Compute API<br/>같은 VM 재시작] BOOT --> SERVICE[systemd<br/>Minecraft 복구]
이 경로에는 네 개의 서로 다른 성공이 있다. 로그 탐지는 종료 원인을 발견했다는 뜻이고,
함수 성공은 API 호출 코드가 끝났다는 뜻이다. VM RUNNING은 게스트가 실행된 상태이고,
Minecraft readiness가 플레이어 관점의 복구다. 알림은 어느 단계를 말하는지 이름에 포함해야
한다.
현재 새 구성이라면 1세대 Cloud Functions 대신 이벤트 기반 Cloud Run function과 Pub/Sub Eventarc 트리거를 선택할 수 있다. Eventarc도 이벤트를 한 번만 전달한다고 가정할 수 없으므로 VM 상태를 읽고 시작 여부를 결정하는 멱등성과 외부 readiness 검증은 그대로 필요하다.
월드를 VM 밖에 남기는 보존 경로
백업 timer는 RCON으로 저장을 요청하고 월드와 설정을 작업 디렉터리에 복사한 뒤 tar와
pigz로 압축해 Cloud Storage에 올렸다. Scheduler와 별도 함수가 오래된 객체를 삭제했다.
flowchart LR accTitle: 월드 백업과 원격 보존 경로 accDescr: 예약 실행, 월드 상태 고정과 작업 복사본 생성, 압축, 원격 업로드와 보존 정책의 네 단계로 월드를 VM 밖에 남긴다. TRIGGER[systemd timer<br/>backup.sh] --> CAPTURE[RCON save-all<br/>world working copy] CAPTURE --> ARCHIVE[tar.gz archive] ARCHIVE --> REMOTE[Cloud Storage<br/>보존 기간 정리]
작업 복사본은 스냅숏이 아니다. cp -r가 여러 파일을 복사하는 동안 Minecraft가 다시
파일을 쓰면 파일별 시점이 달라질 수 있다. save-off, save-all flush, 복사, save-on으로
쓰기 구간을 제어하고 실패해도 save-on이 실행되게 해야 한다. 생성기가 tar.gz를 만들고
복원기가 unzip을 사용한 계약 불일치도 복원 전에 발견돼야 한다.
생성 후 일정 기간이 지난 객체를 지우는 일만 필요하다면 Scheduler, Pub/Sub과 정리 함수
대신 버킷의 Object Lifecycle Management에 Delete 규칙을 둘 수 있다. 사용자별 보존이나
백업 검증 결과처럼 객체마다 다른 판단이 필요할 때는 별도 작업을 유지한다.
운영 당시 구조와 현재 기준
| 영역 | 운영 당시 | 현재 다시 구성한다면 |
|---|---|---|
| VM | 레거시 preemptible = true | Spot provisioning model과 명시적 종료 동작 |
| 복구 함수 | 1세대 Cloud Function, Python 3.10 | Cloud Run functions, Eventarc, 전용 서비스 계정 |
| 권한 | 프로젝트 범위 Instance Admin | 조회·시작·operation 확인만 허용하는 역할 |
| 비밀값 | Terraform 변수, state, metadata, .env | Secret Manager version과 런타임 접근 권한 |
| 부팅 | 부팅마다 설치·다운로드가 많은 script | 이미지·디스크에 고정 의존성, 짧은 초기화 |
| 준비 판정 | VM RUNNING과 Discord 알림 중심 | 외부 Minecraft protocol probe까지 추적 |
| 백업 보존 | Scheduler·Pub/Sub·삭제 함수 | 단순 기간 정책이면 Storage lifecycle |
Terraform에는 secret의 실제 값을 넣지 않고 secret ID와 접근 관계만 둔다. VM에는 전용
서비스 계정을 연결하고 cloud-platform scope와 IAM 역할을 함께 검토한다. OAuth scope가
넓어도 IAM이 거부할 수 있지만, 기본 서비스 계정에 넓은 역할을 주면 scope만으로 경계가
생기지 않는다.
참고
- Google Cloud: Spot VM
- Google Cloud: Spot VM 생성과 사용
- Google Cloud: Compute Engine provisioning model
- Google Cloud: Cloud Run functions 세대 비교
- Google Cloud: Cloud Run function 트리거
- Google Cloud: Eventarc 재시도와 중복 이벤트
- Google Cloud: Pub/Sub 알림 채널
- Google Cloud: Object Lifecycle Management
- Google Cloud: Compute Engine 서비스 계정
Share
아직 댓글이 없습니다. 첫 댓글을 남겨주세요.
검토 대기 중