GCP 선점형 VM 자동 복구 구조

Compute Engine 종료 로그를 Monitoring과 Pub/Sub으로 전달해 VM 자동 복구를 구성한 방법과 탐지·지연·실패의 범위.

운영 당시 Compute Engine은 preemptible = true, automatic_restart = false였다. 레거시 선점형 VM은 플랫폼이 회수할 수 있고 24시간을 넘겨 계속 실행되지 않았다. 종료된 같은 VM을 다시 시작하려면 종료를 관찰하는 주체와 Compute API 호출 권한이 VM 밖에 있어야 했다.

복구 제어 경로는 여섯 단계였다.

sequenceDiagram
  accTitle: 선점 로그에서 Minecraft 재시작까지의 복구 시퀀스
  accDescr: Compute Engine 종료 활동 로그가 Logging, Monitoring, Pub/Sub을 지나 함수에 도착하고 함수가 VM을 시작한 뒤 systemd가 Minecraft를 실행한다.
  participant GCE as Compute Engine
  participant Log as Cloud Logging
  participant Mon as Cloud Monitoring
  participant PS as Pub/Sub
  participant Fn as Cloud Function
  participant API as Compute API
  participant SD as systemd
  GCE->>Log: preempted 활동 로그
  Log->>Mon: counter metric point
  Mon->>PS: incident open
  PS->>Fn: alert payload
  Fn->>API: instances.get
  loop TERMINATED까지
    Fn->>API: instances.get
  end
  Fn->>API: instances.start
  loop RUNNING까지
    Fn->>API: instances.get
  end
  GCE->>SD: OS 부팅
  SD->>SD: Minecraft unit 시작

Cloud Function은 월드 파일이나 JVM을 직접 다루지 않았다. VM 상태 조회와 시작만 담당하고, 부팅 뒤 프로세스 생명주기는 systemd가 맡았다. 이 분리 덕분에 함수에는 SSH 키나 Minecraft 디렉터리 권한이 필요하지 않다.

종료 로그에서 복구 신호까지

실제 로그 기반 메트릭 필터는 선점과 일반 중지 method를 OR로 묶었다. 다음 코드는 실제 형태를 식별자만 일반화한 것이다.

resource "google_logging_metric" "vm_stopped" {
  name        = "minecraft-vm-stopped"
  description = "Detect preemption or stop activity"
  filter      = <<-EOT
    resource.type="gce_instance"
    AND (
      protoPayload.methodName="compute.instances.preempted"
      OR protoPayload.methodName="v1.compute.instances.stop"
    )
  EOT

  metric_descriptor {
    metric_kind = "DELTA"
    value_type  = "INT64"
  }
}

이 필터에는 두 문제가 있다. 첫째, 운영자가 유지보수를 위해 누른 중지도 복구 대상이 된다. 둘째, 프로젝트 안의 특정 instance ID나 zone으로 범위가 제한되지 않는다. 같은 프로젝트에 다른 VM이 추가되면 그 종료가 Minecraft VM 시작 함수까지 자극할 수 있다.

탐지 조건은 실행 정책에서 역으로 만든다.

flowchart TD
  accTitle: 종료 원인에 따른 복구 판단
  accDescr: 플랫폼 선점만 자동 시작하고 운영자 중지, 삭제와 알 수 없는 원인은 자동 시작하지 않는 판단 흐름이다.
  E[종료 활동 로그] --> T{대상 instance·zone인가}
  T -- 아니오 --> IGNORE[기록 후 무시]
  T -- 예 --> C{종료 원인}
  C -- 플랫폼 선점 --> START[자동 시작]
  C -- 운영자 stop --> HOLD[중지 유지]
  C -- 삭제·Terraform 교체 --> BLOCK[자동 시작 금지]
  C -- 분류 불가 --> REVIEW[경보와 수동 확인]

인스턴스 이름은 재생성할 수 있으므로 로그에 안정적으로 제공되는 numeric instance ID를 우선 사용하고 project와 zone도 함께 제한한다. Terraform이 VM을 교체하면 새 ID를 metric filter에 반영하는 의존 관계가 생긴다. 이름으로 제한할 경우 재생성에 강한 대신 동명 자원의 의미를 운영 정책으로 관리해야 한다.

60초 설정 두 개의 의미

Monitoring 조건에는 alignment_period = "60s"duration = "60s"가 함께 있었다.

condition_threshold {
  filter = (
    "resource.type=\"gce_instance\" AND "
    "metric.type=\"logging.googleapis.com/user/minecraft-vm-stopped\""
  )
  comparison      = "COMPARISON_GT"
  threshold_value = 0
  duration        = "60s"

  aggregations {
    alignment_period   = "60s"
    per_series_aligner = "ALIGN_COUNT"
  }
}

alignment period는 원시 시계열을 60초 구간으로 맞춘다. 당시 사용한 ALIGN_COUNT는 그 구간에 포함된 데이터 포인트 개수를 센다. 로그 기반 counter 값의 합을 뜻하지 않는다. 로그 기반 metric이 사건이 없는 구간에 값 0인 point를 기록하면 ALIGN_COUNT의 결과는 0이 아니라 point 한 개를 뜻하는 1이 될 수 있다. threshold > 0과 조합하면 종료 사건이 없어도 조건이 참이 되는 오탐 경로다.

묻고 싶은 값이 “60초 동안 조건에 맞은 종료 로그가 몇 건인가”라면 point의 값을 합산하는 ALIGN_SUM을 사용해야 한다.

aggregations {
  alignment_period   = "60s"
  per_series_aligner = "ALIGN_SUM"
}

duration은 이렇게 정렬된 조건이 얼마나 오래 참이어야 incident를 열지 정한다. alignment period와 duration을 더하거나 겹쳐서 “정확히 60초 뒤 함수 실행”으로 해석할 수 없다. 변경 뒤에는 0인 구간, 실제 종료 한 건과 여러 건을 synthetic log로 넣어 incident open 여부를 각각 검증해야 한다.

timeline
  accTitle: 종료부터 자동 복구까지 누적되는 시간
  accDescr: 종료 로그 생성 뒤 Logging 수집, 메트릭 정렬, alert 평가, Pub/Sub 전달, 함수 polling과 VM 부팅 시간이 차례로 누적된다.
  title 복구 지연을 구성하는 단계
  종료 시각 : Compute 활동 로그 생성
  Logging : 로그 수집과 metric point 생성
  Monitoring : 60초 구간 정렬 : 조건 지속 시간 평가
  Pub/Sub : incident 메시지 발행과 전달
  함수 : 종료 상태 polling : start 요청 : 실행 상태 polling
  VM : OS 부팅 : systemd : Minecraft 준비

실제 복구 시간을 구하려면 다음 timestamp를 같은 recovery_id로 남겨야 한다.

시각측정 위치알 수 있는 지연
event_timeCompute 활동 로그선점 발생 기준점
incident_opened_atMonitoring payload탐지·평가 지연
function_received_at함수 첫 로그Pub/Sub 전달·cold start 지연
start_requested_atCompute operation함수 판단 지연
vm_running_atCompute 상태 조회provisioning 지연
minecraft_ready_at외부 protocol probeOS·systemd·JVM 준비 지연

설정값만 남기면 어느 단계가 늦었는지 분리할 수 없다. Discord 메시지 시각은 사용자 알림에는 유용하지만 원본 운영 기록으로 삼기 어렵다. webhook 실패와 전송 지연이 복구 상태를 바꾸지 않기 때문이다.

Monitoring에서 Pub/Sub으로 이어지는 전달 경로

Monitoring notification channel이 Pub/Sub topic을 가리키고, Monitoring 서비스 에이전트에 topic publish 권한을 부여했다.

resource "google_monitoring_notification_channel" "restart" {
  display_name = "Minecraft VM restart"
  type         = "pubsub"
  labels = {
    topic = google_pubsub_topic.vm_restart.id
  }
}

resource "google_pubsub_topic_iam_member" "monitoring_publish" {
  topic  = google_pubsub_topic.vm_restart.name
  role   = "roles/pubsub.publisher"
  member = "serviceAccount:service-${data.google_project.current.number}@gcp-sa-monitoring-notification.iam.gserviceaccount.com"
}

여기서 topic에 publish할 권한과 topic의 메시지를 처리할 함수 권한은 별개다. notification channel은 Monitoring이 메시지를 발행할 수 있게 하고, 함수 trigger는 Pub/Sub 메시지가 실행 환경에 전달되게 한다. 하나의 서비스 계정에 두 역할을 모두 맡길 필요가 없다.

Pub/Sub 메시지는 기본적으로 at-least-once로 처리한다. 같은 incident의 open 알림이 다시 도착하거나 함수 실행이 재시도될 수 있다. 알림 정책의 incident close도 별도 메시지다. 실제 함수는 state != "open"인 메시지에서도 VM이 TERMINATED면 시작을 시도했다. close를 단순 성공 통지로만 볼 수 없었던 코드다.

def handle_alert(payload):
    incident = payload.get("incident", {})
    state = incident.get("state", "unknown")

    if state != "open":
        status = get_instance_status()
        if status == "TERMINATED":
            start_vm_and_wait()
        return

close 사건에서 재시작하는 정책은 복구 누락을 보완할 수 있지만 의도한 수동 중지를 다시 켜는 경로도 하나 더 만든다. incident_id, 상태 전이, 종료 원인과 대상 VM을 함께 확인하고 close는 기존 복구 기록을 닫는 데만 사용하는 쪽이 판단하기 쉽다.

함수의 상태 polling

종료 로그가 함수에 도착했을 때 VM이 아직 STOPPING일 수 있다. 함수는 5초 간격으로 최대 45초 동안 TERMINATED를 기다린 뒤 start를 호출하고, 다시 최대 120초 동안 RUNNING을 기다렸다.

def wait_until_terminated(compute, project, zone, instance,
                          timeout=45, interval=5):
    for _ in range(0, timeout, interval):
        vm = compute.instances().get(
            project=project, zone=zone, instance=instance
        ).execute()
        if vm.get("status") == "TERMINATED":
            return True
        time.sleep(interval)
    return False

45초와 120초는 Compute Engine 보장이 아니라 운영 코드에 적힌 한도다. API 오류도 같은 loop 안에서 출력하고 계속 기다렸기 때문에 권한 오류, 일시 오류와 정상 상태 전이가 timeout 하나로 합쳐질 수 있었다. 재시도 가능한 HTTP 오류와 즉시 중단할 인증·권한 오류를 나누고 마지막 status와 오류 코드를 함께 남겨야 한다.

instances.start 응답은 장기 실행 operation이다. start 호출이 HTTP 성공했다고 VM이 실행됐다는 뜻은 아니다. operation ID를 저장하고 DONEerror를 확인한 뒤 instance 상태 polling으로 이어가야 원인을 보존할 수 있다.

현재 Spot VM에 적용할 기준

현재 Spot VM에는 24시간 만료가 없다. 대신 Compute Engine이 언제든 선점할 수 있고 SLA, 자동 재시작과 live migration을 제공하지 않는다. 기본적으로 best-effort 종료 알림 뒤 VM이 중지되거나 삭제된다. 같은 VM과 디스크를 다시 사용할 목적이라면 종료 동작을 STOP으로 명시한다.

scheduling {
  provisioning_model          = "SPOT"
  instance_termination_action = "STOP"
  automatic_restart           = false
  on_host_maintenance         = "TERMINATE"
}

단일 Minecraft 서버에서 같은 VM을 다시 켜는 설계는 고정 IP와 디스크 상태를 유지하기 쉽다. 반면 다음 조건이면 MIG나 일반 VM이 더 맞다.

  • 중단 허용 시간이 짧고 Spot 용량 부족 중에도 즉시 복구돼야 한다.
  • immutable image에서 동일 서버를 재생성할 수 있다.
  • 월드 상태가 별도 저장소와 복원 절차로 분리돼 있다.
  • 자동 치유와 zone 분산의 복잡도를 감수할 수 있다.

복구 함수가 start를 요청해도 해당 zone에 Spot 용량이 없으면 계속 실패할 수 있다. 같은 machine type 재시도, 다른 zone의 새 VM 생성, 일반 VM으로 전환 중 어느 동작을 허용할지는 월드 디스크와 고정 IP 이동 가능성을 포함해 별도 정책으로 정해야 한다.

복구 제어면의 최소 권한과 실패 경로

실제 함수에는 프로젝트 범위 roles/compute.instanceAdmin.v1roles/logging.viewer가 부여됐다. 함수가 로그를 직접 검색하지 않고 Pub/Sub payload만 읽는다면 Logging Viewer는 필요하지 않다. Instance Admin은 VM 시작보다 훨씬 넓은 작업을 허용한다.

필요한 작업은 대상 instance 조회, start 요청과 zone operation 조회다. 전용 서비스 계정과 custom role을 만들고 대상 프로젝트에서 역할을 부여하되 코드에서도 instance·zone allowlist를 고정한다. IAM은 보통 개별 VM보다 상위 범위에서 부여되는 경우가 있어 애플리케이션 검증을 두 번째 경계로 사용해야 한다.

자동 경로와 별개로 수동 복구 절차를 남긴다.

  1. incident와 함수 실행 로그에서 recovery_id를 찾는다.
  2. 대상 VM의 현재 상태와 최근 operation 오류를 확인한다.
  3. 선점이 맞고 TERMINATED면 명시적으로 start한다.
  4. serial console·startup script·systemd 로그를 확인한다.
  5. 외부 Minecraft status가 성공한 뒤 복구 완료를 기록한다.

참고

Share

공유

이미지 확대