VM 재시작과 Minecraft readiness 검증
중복 메시지와 Compute operation을 처리하고 VM 실행 뒤 Minecraft 준비 상태까지 확인한 방법
복구 함수의 HTTP 응답, VM의 RUNNING, systemd의 active와 Minecraft 접속 가능 상태는
서로 다른 완료 조건이다. 운영 코드에는 TERMINATED와 RUNNING을 기다리는 polling이
있었지만 Compute operation 오류, Pub/Sub 중복 사건과 Minecraft protocol readiness는
끝까지 연결되지 않았다.
Terraform은 미리 압축된 ZIP을 배포했다. 저장소에 남은
ZIP 내부 main.py와 바깥 Python 소스는 해시와 제어 흐름이 달랐다. 운영 함수를 설명하려면
바깥 소스가 아니라 실제 배포 산출물을 기준으로 삼아야 했다.
소스에서 실행 코드까지의 경로
실제 1세대 함수는 기존 ZIP의 파일 해시를 객체 이름에 넣어 Cloud Storage에 업로드했다.
resource "google_storage_bucket_object" "function_zip" {
name = "restart-${filesha256("function.zip")}.zip"
bucket = google_storage_bucket.function_source.name
source = "function.zip"
}
resource "google_cloudfunctions_function" "restart_vm" {
name = "restart-minecraft-vm"
runtime = "python310"
source_archive_bucket = google_storage_bucket.function_source.name
source_archive_object = google_storage_bucket_object.function_zip.name
entry_point = "restart_vm"
}
filesha256는 주어진 ZIP의 변경만 Terraform에 알린다. Python 소스와 ZIP의 일치 여부는
별도 검사 대상이다. 사람이 ZIP을 갱신하지 않으면 소스만 고친 변경은 배포 계획에 나타나지
않는다.
flowchart LR accTitle: 복구 함수 소스와 배포 산출물의 연결 accDescr: 함수 소스와 잠긴 의존성을 하나의 검증된 ZIP으로 만들고, Terraform이 그 산출물을 함수 revision과 배포 기록으로 연결한다. SRC[main.py] --> PACKAGE[clean build<br/>deterministic ZIP<br/>SHA-256 검사] REQ[잠긴 requirements] --> PACKAGE PACKAGE --> RELEASE[Terraform plan<br/>Cloud Storage object] RELEASE --> DEPLOY[function revision<br/>revision·hash 기록]
재현 가능한 패키징에서는 이전 ZIP을 덮어쓰지 않는다. 새 임시 디렉터리에 필요한 소스와 의존성만 복사하고 timestamp·파일 순서를 고정한 뒤 압축한다. ZIP 내부 파일 목록, entry point 존재, 의존성 버전과 SHA-256을 검사하고 그 경로를 Terraform에 전달한다. 배포 뒤에는 함수 revision과 같은 hash를 구조화 로그 또는 release 기록에 남긴다.
Terraform의 archive_file data source를 사용하면 소스 변경을 계획에 연결할 수 있다.
data "archive_file" "restart_function" {
type = "zip"
source_dir = "${path.module}/function"
output_path = "${path.module}/build/restart-function.zip"
}
resource "google_storage_bucket_object" "function_zip" {
name = "restart-${data.archive_file.restart_function.output_sha256}.zip"
bucket = google_storage_bucket.function_source.name
source = data.archive_file.restart_function.output_path
}
의존성을 ZIP에 vendoring한다면 패키지 설치까지 별도 build step으로 고정해야 한다.
archive_file만으로 Python dependency가 자동 설치되지는 않는다. 런타임이
requirements.txt를 설치하는 배포 방식이라면 source archive에 lock된 명세를 포함한다.
중복을 전제로 한 메시지 처리
Pub/Sub은 기본적으로 at-least-once 전달을 제공한다. 함수가 결과를 기록하기 전에 죽거나 acknowledgement가 유실되면 같은 메시지가 다시 처리될 수 있다. 서로 다른 Monitoring incident가 같은 VM 종료를 가리킬 수도 있다.
가장 위험한 구간은 check-then-act다.
실행 A: VM 상태 TERMINATED 확인
실행 B: VM 상태 TERMINATED 확인
실행 A: instances.start 호출
실행 B: instances.start 호출
두 번째 start가 단순 오류로 끝날 수 있어도 로그와 알림이 실패처럼 보이고, 다른 상태 변경 API에서는 실제 중복 부작용이 생길 수 있다. 사건 ID와 API 요청 ID를 연결해 같은 의미의 호출을 같은 연산으로 취급한다.
from uuid import NAMESPACE_URL, uuid5
SERVICE_NAMESPACE = uuid5(
NAMESPACE_URL, "urn:juntiger:minecraft-recovery"
)
def request_id_for(incident_id: str) -> str:
return str(uuid5(SERVICE_NAMESPACE, incident_id))
operation = compute.instances().start(
project=project,
zone=zone,
instance=instance,
requestId=request_id_for(incident_id),
).execute()
UUID v5는 같은 incident ID에서 같은 UUID를 만든다. 무작위 UUID를 매 실행마다 만들면 중복 억제 효과가 없다. 서비스별 namespace를 따로 정하면 다른 종류의 ID가 같은 문자열을 써도 요청 ID가 겹치지 않는다.
상태별 동작도 멱등적으로 만든다.
| 현재 상태 | 처리 |
|---|---|
RUNNING | 성공 no-op, 기존 operation과 readiness 확인 |
PROVISIONING·STAGING | start 재호출 없이 기존 전이 관찰 |
STOPPING | 제한 시간 동안 TERMINATED 대기 |
TERMINATED | 안정적인 requestId로 start |
| 상태 조회 실패 | 오류 종류 기록, 권한 오류는 즉시 중단 |
VM 한 대를 복구하는 현재 범위에서는 상태별 no-op과 안정적인 request ID부터 적용한다. 복구 대상이나 중복 부작용이 늘어나면 별도 DB나 분산 lock을 추가한다.
Compute operation 완료 대기
실제 함수의 시작 코드는 start 응답을 버리고 VM 상태만 polling했다.
def start_vm_and_wait(compute, project, zone, instance):
compute.instances().start(
project=project, zone=zone, instance=instance
).execute()
for _ in range(0, 120, 5):
vm = compute.instances().get(
project=project, zone=zone, instance=instance
).execute()
if vm.get("status") == "RUNNING":
return True
time.sleep(5)
return False
Compute Engine의 start 응답은 zone operation이다. operation의 status가 DONE이 돼도
error가 있을 수 있다. API 연산 실패와 VM 상태 전이 지연을 분리하려면 operation을 먼저
기다린다.
def wait_for_zone_operation(compute, project, zone, operation_name):
while True:
operation = compute.zoneOperations().get(
project=project,
zone=zone,
operation=operation_name,
).execute()
if operation["status"] == "DONE":
if "error" in operation:
raise RuntimeError(operation["error"])
return operation
time.sleep(2)
operation은 API 작업의 완료와 오류를, instance status는 대상 자원의 현재 상태를 말한다. 두 값을 차례로 확인해야 start 요청과 VM 상태 전이를 각각 검증할 수 있다. timeout 로그에는 operation name, 마지막 operation status, 마지막 instance status와 elapsed time을 함께 남긴다.
stateDiagram-v2 accTitle: Compute Engine과 Minecraft 준비 상태 accDescr: VM은 STOPPING에서 TERMINATED를 거쳐 PROVISIONING, STAGING, RUNNING으로 바뀌고 그 뒤 OS와 systemd, JVM 준비를 거쳐 READY가 된다. [*] --> STOPPING: 종료 이벤트 STOPPING --> TERMINATED TERMINATED --> PROVISIONING: instances.start PROVISIONING --> STAGING STAGING --> RUNNING RUNNING --> OS_READY: 부팅 완료 OS_READY --> SERVICE_ACTIVE: systemd active SERVICE_ACTIVE --> LISTENING: TCP·UDP bind LISTENING --> READY: 외부 status 성공 SERVICE_ACTIVE --> FAILED: 재시작 반복·초기화 오류
VM RUNNING 다음의 readiness
startup script는 OS 부팅 뒤 실행된다. 패키지 설치, 외부 다운로드, 백업 복원, 파일 권한과
unit 생성 중 하나가 실패해도 Compute Engine은 VM을 RUNNING으로 표시할 수 있다.
systemd active도 Java 프로세스가 살아 있다는 뜻이지 월드와 플러그인 초기화가 끝났다는
뜻은 아니다.
sequenceDiagram accTitle: 계층별 Minecraft readiness 검사 accDescr: 함수가 VM 실행을 확인한 뒤 게스트 에이전트, systemd, 로컬 포트, 서버 로그와 외부 Minecraft protocol을 차례로 확인한다. participant Fn as 복구 함수 participant API as Compute API participant OS as guest OS participant SD as systemd participant MC as Minecraft participant Ext as 외부 probe Fn->>API: operation DONE·VM RUNNING 확인 API-->>Fn: RUNNING OS->>SD: minecraft.service 시작 SD->>MC: Java 실행 MC->>MC: 월드·플러그인 로드 MC-->>OS: Done 메시지·local listener Ext->>MC: public IP로 status 요청 MC-->>Ext: protocol response Ext-->>Fn: READY 기록
VM 안의 검사와 밖의 검사는 발견하는 실패가 다르다.
systemctl is-active --quiet minecraft.service
journalctl -u minecraft.service -b --no-pager | tail -n 100
ss -lnt '( sport = :25565 )'
로컬 TCP 리스너 성공은 JVM이 포트를 열었다는 증거다. 외부 IP, VPC 방화벽과 경로는
검사하지 못한다. Geyser의 Bedrock UDP는 ss -lnu로 별도 확인한다. 마지막 probe는 VM
밖에서 Java server list ping과 Bedrock status를 각각 수행하고 응답에 기대한 server
identity나 version을 확인한다.
Done (...) 문자열만 검사하면 버전과 로그 형식에 결합된다. 가능한 경우 Minecraft
protocol probe를 최종 조건으로 사용하고 로그는 원인 분석 자료로 남긴다. 성공 알림에는
VM_RUNNING과 MINECRAFT_READY를 구분해 잘못된 기대를 만들지 않는다.
Cloud Run functions 전환 시 변경점
현재 2세대 함수는 Cloud Run functions로 제공된다. Terraform에서는
google_cloudfunctions2_function의 build와 service 설정을 분리하고 Pub/Sub Eventarc
trigger를 선언할 수 있다.
resource "google_cloudfunctions2_function" "restart_vm" {
name = "restart-minecraft-vm"
location = var.region
build_config {
runtime = "python312"
entry_point = "restart_vm"
source {
storage_source {
bucket = google_storage_bucket.function_source.name
object = google_storage_bucket_object.function_zip.name
}
}
}
service_config {
available_memory = "256M"
timeout_seconds = 300
service_account_email = google_service_account.restart.email
max_instance_count = 1
max_instance_request_concurrency = 1
}
event_trigger {
trigger_region = var.region
event_type = "google.cloud.pubsub.topic.v1.messagePublished"
pubsub_topic = google_pubsub_topic.vm_restart.id
retry_policy = "RETRY_POLICY_RETRY"
}
}
max_instance_count = 1과 인스턴스당 동시성 1은 평상시 병렬 실행을 줄인다. 급격한 요청
증가 때는 최대 인스턴스 설정을 일시적으로 넘을 수 있고, Pub/Sub 재전달이나 서로 다른
revision의 겹침도 남는다. 두 값을 모두 1로 둬도 정확히 한 번 실행을 보장하지 않으므로
handler가 여전히 멱등적이어야 한다. timeout을 300초로 늘려도 VM과 Minecraft가 그 안에
준비된다는 보장은 없다. 장시간 readiness는 recovery 상태를 기록하고 별도 probe가 갱신하면
단계별 지연과 실패를 각각 관찰할 수 있다.
권한과 구조화 로그
실제 함수는 기본 서비스 계정에 프로젝트 범위 roles/compute.instanceAdmin.v1과
roles/logging.viewer를 사용했다. 새 함수에는 전용 서비스 계정을 연결하고 실제 API
호출에서 권한을 역산한다.
- instance 상태 조회
- instance start
- zone operation 조회
- 함수 로그 작성
- 필요하면 Secret Manager의 특정 webhook version 접근
함수가 활동 로그를 직접 검색하지 않으면 Logs Viewer는 제외한다. 거부 테스트도 포함한다. 대상 VM 시작은 성공하고 다른 VM 시작, 삭제, 디스크 변경과 방화벽 수정은 실패해야 한다.
모든 로그에 다음 필드를 공통으로 둔다.
{
"recovery_id": "example-incident-id",
"event_state": "open",
"instance": "minecraft-server",
"phase": "WAITING_FOR_READY",
"operation": "operation-name",
"elapsed_ms": 84231,
"result": "pending"
}
사건 시간축은 배포 revision과 source hash까지 포함한다. 그래야 운영 중 관찰한 동작을 어느 코드가 만들었는지 연결할 수 있다.
timeline accTitle: 한 복구 사건의 상관관계 시간축 accDescr: incident ID를 기준으로 메시지 수신, operation, VM 상태, systemd, protocol readiness와 최종 알림이 이어진다. title recovery_id로 연결할 기록 Incident : alert open Function : message received : source hash 기록 Compute : start operation created : operation DONE Instance : RUNNING Guest : startup script 종료 : systemd active Minecraft : protocol READY Notify : 복구 완료 알림
참고
- Google Cloud: Pub/Sub subscription overview
- Google Cloud: Pub/Sub exactly-once delivery
- Compute Engine API: instances.start
- Google Cloud: Compute API 요청과 응답
- Google Cloud: Cloud Run functions 세대 비교
- Google Cloud: Cloud Run 최대 동시성 설정
- Google Cloud: Terraform으로 함수 배포
- Terraform Registry: Cloud Functions 2nd generation
- Google Cloud: Linux VM startup scripts
Comments
아직 댓글이 없습니다. 첫 댓글을 남겨주세요.
검토 대기 중