Spot VM 선점 뒤 Minecraft 서버 다시 시작하기

Spot 선점만 골라 같은 VM을 다시 시작하는 별도 Terraform 구성

서버의 compute.tf는 Spot 선점 종료 동작을 STOP으로 설정하므로 VM이 선점되면 TERMINATED로 남는다. 일정마다 시작 명령을 보내는 Scheduler는 운영자가 비용이나 점검 때문에 멈춘 서버도 다시 켠다. 아래 구성은 Google Cloud가 기록한 compute.instances.preempted 사건만 골라 같은 VM을 시작한다.

선수 조건

08편의 수동 시작과 장애 확인 절차가 먼저 동작해야 한다. 자동화가 실패했을 때 사람이 시작할 수 없는 서버에는 복구 함수를 추가하지 않는다.

검증 범위

Terraform 정적 검사와 Python 분기 테스트를 로컬에서 실행했다. Cloud Run functions, Pub/Sub, Logging sink를 만들거나 실제 선점을 유발하지 않았다. 이 구성의 상태는 로컬과 컨테이너 검증 완료, 실제 Google Cloud apply와 선점 미검증이다.

서버와 복구 루트를 나눈 이유

복구 코드는 VM이 꺼진 뒤에도 실행돼야 한다. 서버 루트의 startup script 대신 별도로 배포한 Cloud Run function에서 실행한다.

Logging sink는 조건에 맞는 로그만 골라 내보낸다. Pub/Sub topic은 그 사건을 메시지로 보관해 Cloud Run functions에 전달한다. 함수는 메시지를 받은 때만 짧게 실행된다.

sequenceDiagram
  accTitle: Spot 선점 뒤 VM 자동 복구
  accDescr: 선점 시스템 이벤트가 Logging sink와 Pub/Sub을 지나 복구 함수로 전달되고, 함수가 대상과 상태를 다시 확인한 뒤 같은 VM을 시작한다.
  participant VM as Spot VM
  participant LOG as Cloud Logging
  participant TOPIC as Pub/Sub
  participant FN as 복구 함수
  participant API as Compute API
  VM->>LOG: compute.instances.preempted
  LOG->>TOPIC: 필터가 맞는 LogEntry
  TOPIC->>FN: CloudEvent
  FN->>FN: 프로젝트, 존, VM, 메서드 재검증
  FN->>API: instances.get
  alt VM이 TERMINATED
    FN->>API: instances.start
    FN->>API: operation 결과 확인
  else 시작 중이거나 실행 중
    FN-->>FN: no_action 기록
  end

서버 루트의 state에는 VM, 디스크, 네트워크와 고정 IP가 들어간다. 복구 루트의 state에는 Logging sink, Pub/Sub, 함수와 복구 서비스 계정만 넣는다. 서로 다른 backend prefix를 사용하므로 각 삭제 계획에는 해당 루트의 리소스만 나타난다.

예제와 입력값 준비

서버 루트와 나란한 디렉터리에서 예제를 받는다.

실행

cd "${HOME}"
curl --fail --location \
  "https://juntiger-assets.pages.dev/minecraft-spot-recovery.zip" \
  --output minecraft-spot-recovery.zip
unzip -q minecraft-spot-recovery.zip
cd minecraft-spot-recovery

cp backend.hcl.example backend.hcl
cp terraform.tfvars.example terraform.tfvars
nano backend.hcl

03편에서 만든 state 버킷을 사용하되 prefix는 다르게 적는다.

bucket = "내-state-버킷-이름"
prefix = "minecraft/spot-recovery"

terraform.tfvars에는 서버 루트의 출력과 함수 소스 버킷 이름을 넣는다. 옮겨 적기 전에 서버 루트의 실제 출력값을 화면에 띄워 둔다.

확인: 서버 루트 출력

(cd "${HOME}/minecraft-one-root" && terraform output)

실행

nano terraform.tfvars
project_id                  = "내-프로젝트-ID"
region                      = "asia-northeast3"
zone                        = "asia-northeast3-a"
instance_name               = "minecraft"
function_source_bucket_name = "전-세계에서-고유한-함수-소스-버킷"

project_id, zone과 instance_name은 서버 루트의 출력과 정확히 같아야 한다. 이 루트에는 서버 루트의 scripts/preflight.sh 같은 사전 검사가 없다. 변수 검증은 프로젝트·버킷 자리 표시자와 instance 이름 형식을 거부하지만 서버 루트 출력과의 일치까지 확인하지 않는다. 오타가 있어도 plan까지 통과할 수 있으므로 위 출력과 글자 단위로 대조한다. 함수 소스 버킷은 백업 버킷과 state 버킷에 사용하지 않은 이름을 쓴다. 이후 명령은 별도 경로가 적힌 경우를 제외하고 Cloud Shell의 ${HOME}/minecraft-spot-recovery에서 실행한다.

함수의 로컬 분기 시험

실제 선점을 기다리기 전에 입력 분류와 API 호출 조건을 로컬 Python 테스트로 확인한다.

실행

python3 -m venv .venv
. .venv/bin/activate
pip install -r function/requirements.txt
PYTHONPATH=function python -m unittest -v function/test_main.py

첫 명령이 ensurepip is not available 오류로 실패하면 실행 환경에 venv 모듈이 없다는 뜻이다. Ubuntu 환경이라면 sudo apt-get install python3-venv로 설치한 뒤 다시 실행한다.

테스트는 다음 입력을 다룬다.

  • 올바른 선점 사건과 TERMINATED VM
  • 수동 중지와 다른 메서드
  • 다른 프로젝트, 존 또는 VM
  • Pub/Sub data 누락과 잘못된 JSON
  • 중복 전달 때 PROVISIONING, STAGING, RUNNING 상태
  • STOPPING에서 TERMINATED로 바뀌는 상태와 대기 시간 초과
  • 15분보다 오래된 사건
  • Compute API 오류와 operation timeout

정상 결과의 마지막 줄은 다음 형태다.

Ran ... tests in ...

OK

한 테스트라도 FAILED면 Terraform 적용으로 가지 않는다. Python 패키지 설치만 로컬 환경을 바꾸며 Google Cloud 리소스는 만들지 않는다.

로그와 VM 상태의 이중 확인

Logging sink는 다음 조건을 모두 만족하는 시스템 이벤트만 Pub/Sub으로 보낸다.

log_id("cloudaudit.googleapis.com/system_event")
protoPayload.serviceName="compute.googleapis.com"
protoPayload.methodName="compute.instances.preempted"
protoPayload.resourceName="projects/PROJECT/zones/ZONE/instances/INSTANCE"

운영자의 v1.compute.instances.stop, Terraform 삭제와 교체는 이 필터에 맞지 않는다. 함수도 전달받은 LogEntry의 method와 전체 resourceName을 환경 변수의 허용 대상과 다시 비교한다.

입력이 맞아도 log timestamp가 현재보다 15분 넘게 오래됐으면 무시한다. Compute API에서 상태가 STOPPING이면 제한 시간 동안 다시 조회한다. TERMINATED가 된 뒤에만 instances.start를 호출하고 operation이 끝날 때까지 확인한다. 중복 Pub/Sub 전달이 도착했을 때 VM이 이미 PROVISIONING, STAGING 또는 RUNNING이면 no_action으로 끝난다.

복구 계정의 권한

함수 runtime 서비스 계정의 custom role에는 세 권한만 넣는다.

compute.instances.get
compute.instances.start
compute.zoneOperations.get

이 계정은 VM 삭제, 머신 유형 변경과 디스크 작업을 할 수 없다. 함수 배포 과정에는 Cloud Functions, Cloud Build, Artifact Registry, Eventarc, Pub/Sub, Logging과 Storage API도 필요하다. Terraform 실행 계정에는 이 리소스를 만들 권한과 build, runtime 서비스 계정을 사용할 권한이 있어야 한다.

한 가지 예외를 그대로 읽어야 한다. 함수 소스를 컨테이너로 빌드하기 위해 예제의 iam.tf는 Compute Engine 기본 서비스 계정에 프로젝트 수준 roles/cloudbuild.builds.builder를 부여한다. 이 역할은 runtime custom role보다 범위가 넓고 빌드 단계에만 필요하므로, 공동 프로젝트라면 관리자와 부여 여부를 먼저 상의한다.

Eventarc trigger는 별도 서비스 계정을 쓴다. 이 계정에는 roles/eventarc.eventReceiver와 roles/run.invoker만 연결하고, 함수의 event_trigger.service_account_email에 명시한다. runtime 계정만 VM 시작 권한을 가지며 trigger 계정은 사건 수신과 함수 호출만 맡는다.

공동 프로젝트에서는 아래 작업 범위를 관리자에게 전달한다.

작업대표 역할
API 활성화roles/serviceusage.serviceUsageAdmin
Logging sink 생성roles/logging.configWriter
Pub/Sub topic과 IAMroles/pubsub.admin
함수 생성roles/cloudfunctions.admin
함수 소스 버킷roles/storage.admin
서비스 계정과 custom roleroles/iam.serviceAccountAdmin, roles/iam.roleAdmin
계정 연결과 프로젝트 IAMroles/iam.serviceAccountUser, 프로젝트 IAM 변경 권한

복구 루트의 첫 plan

실행: 아직 복구 리소스를 만들지 않는다

terraform init -backend-config=backend.hcl
terraform fmt
terraform fmt -check -recursive
terraform validate
terraform plan -out=recovery.tfplan
terraform show recovery.tfplan

계획에서 sink filter의 프로젝트, 존, instance, custom role의 세 권한, 별도 함수 소스 버킷, runtime 계정과 Eventarc trigger 계정을 확인한다. 함수 ZIP의 제외 목록에는 __pycache__와 test_main.py가 있어야 한다. 서버 루트가 소유한 VM이나 네트워크가 계획에 나오면 중단한다.

복구 리소스 적용

다음 명령은 API를 켜고 Pub/Sub, Storage, Logging과 Cloud Run functions 관련 리소스를 만든다. 사용량이 작아도 무료라고 가정할 수 없다. 예산 알림과 삭제 경로를 확인하지 않았다면 실행하지 않는다.

비용 발생: 자동 복구 비용과 권한을 승인했을 때

terraform apply recovery.tfplan

Apply complete! 뒤 출력값에서 sink, 함수와 복구 대상 VM을 확인한다. 중간 실패는 일부 리소스를 남길 수 있으므로 새 plan에서 현재 상태를 다시 읽는다.

실행: 복구 루트에서 대상값 저장

export TARGET_PROJECT_ID="$(
  terraform output -raw target_project_id
)"
export TARGET_ZONE="$(
  terraform output -raw target_zone
)"
export TARGET_INSTANCE="$(
  terraform output -raw target_instance_name
)"
printf 'Recovery target: %s / %s / %s\n' \
  "${TARGET_PROJECT_ID}" \
  "${TARGET_ZONE}" \
  "${TARGET_INSTANCE}"

세 값이 서버 루트의 출력과 다르면 수동 중지 시험을 실행하지 않는다.

수동 중지 시험

실제 복구 루트를 적용한 독자만 실행한다. 운영자가 멈춘 VM은 자동 복구하지 않아야 한다.

조건부 실행: 복구 루트를 적용한 경우

gcloud compute instances stop "${TARGET_INSTANCE}" \
  --project="${TARGET_PROJECT_ID}" \
  --zone="${TARGET_ZONE}"

몇 분 뒤에도 TERMINATED여야 한다. 자동으로 켜졌다면 함수 로그보다 먼저 Logging sink 필터와 다른 자동화가 있는지 확인하고 복구 루트를 비활성화한다.

시험 뒤 수동으로 시작한다.

gcloud compute instances start "${TARGET_INSTANCE}" \
  --project="${TARGET_PROJECT_ID}" \
  --zone="${TARGET_ZONE}"

실제 선점 관찰

Google Cloud가 Spot VM을 선점하는 시점은 운영자가 정할 수 없다. 제품 상태와 VM 설정에 따라 maintenance simulation을 사용할 수 있더라도 실제 월드가 있는 서버에 무리하게 사건을 만들지 않는다.

함수 로그의 결과는 다음처럼 구분된다.

결과의미와 다음 행동
ignored_event메시지가 잘못됐거나, 메서드 또는 대상이 다르거나, 15분보다 오래됐거나, 권한 부족 같은 영구 오류(permanent_compute_error)라서 시작하지 않음
no_actionVM이 이미 시작 중이거나 실행 중
start_requested검증된 선점 뒤 TERMINATED VM에 시작 요청
결과 없이 오류 로그만 남음operation timeout이나 Spot 용량 부족 같은 일시 오류는 예외로 전파되고, Eventarc 재시도 정책(RETRY_POLICY_RETRY)이 같은 사건을 다시 전달함

함수가 시작을 요청해도 해당 존에 Spot 용량이 없으면 실패할 수 있다. 복구 함수는 다른 존에 새 VM을 만들거나 일반 VM으로 바꾸지 않는다. 고정 IP와 월드 디스크의 소유권까지 옮기는 별도 복구 설계가 필요하기 때문이다.

실제 선점과 start_requested 뒤 Minecraft의 새 Done (...)!까지 관찰했다면 실환경 시험 날짜를 운영 기록에 남긴다. 그렇지 않으면 완료 상태를 과장하지 않고 로컬과 컨테이너 검증 완료, 실제 Google Cloud apply와 선점 미검증으로 유지한다.

자동 복구 실패와 수동 시작

확인

gcloud compute instances describe "${TARGET_INSTANCE}" \
  --project="${TARGET_PROJECT_ID}" \
  --zone="${TARGET_ZONE}" \
  --format="value(status)"

조건부 실행: 상태가 TERMINATED이고 사람이 복구할 때

gcloud compute instances start "${TARGET_INSTANCE}" \
  --project="${TARGET_PROJECT_ID}" \
  --zone="${TARGET_ZONE}"

VM이 RUNNING이 되면 05편의 startup script → systemd → Done (...)! 순서로 Minecraft 준비 상태를 확인한다. 설계 원리는 GCP 선점형 VM 자동 복구 구조와 복구 readiness와 배포에서 더 자세히 다룬다.

완료 체크

  • Python 단위 테스트가 모두 통과했다.
  • 서버와 복구 루트가 서로 다른 backend prefix를 사용한다.
  • 수동 중지는 자동으로 시작하지 않는다.
  • 실제 선점을 보지 않았다면 미검증 상태를 기록했다.
  • 수동 시작 절차를 계속 사용할 수 있다.

자동 복구를 적용한 서버는 마지막 백업과 리소스 삭제에서 복구 루트를 서버 루트보다 먼저 제거한다.

참고

Comments

댓글

    이미지 확대