16진행자·운영자 Isaac Sim · EKS/예상 약 45분, 별도 일정

체크포인트와 새 Spot 노드 복구 시험

안정된 완료 지점을 저장하고 통제된 노드 교체로 재개를 검증합니다.

진행자·운영자 Isaac Sim · EKS · 별도 트랙아직 읽지 않음

진행자·운영자 선택 트랙, 예상 약 45분입니다. 실제 성공한 Isaac 실행이 선행되어야 합니다. 노드 확보·이미지·초기화 시간은 환경에 따라 더 걸립니다.

이 장은 운영자 시험 작업의 NodeClaim과 Pod를 명시적으로 교체하는 애플리케이션 복구 리허설입니다. AWS FIS나 실제 EC2 Spot 회수 알림을 주입하는 시험이 아닙니다. 결과의 actualSpotInterruptionInjected도 false입니다.

FIS 지원 범위

AWS 공식 문서는 Auto Mode에서 ec2:SendSpotInstanceInterruptions를 포함한 일부 FIS EC2 작업을 지원하지 않는다고 설명하며, FIS EKS Pod 작업은 지원한다고 명시합니다. FIS 전체나 모든 중단 시험이 불가능하다고 해석하지 않습니다.

방식 구분
FIS ec2:SendSpotInstanceInterruptions Auto Mode 미지원 EC2 작업
FIS EKS Pod 작업 지원되는 별도 시험 방식. 실험 대상·권한·중지 조건을 별도 검토
이 장의 recover FIS를 호출하지 않고 소유 NodeClaim·시험 Pod를 교체
실제 Spot 회수 관측 실제 알림·종료·재개 시각을 따로 수집해야 하는 운영 근거

지원 범위의 근거는 AWS Auto Mode 인스턴스 문서 (새 탭)입니다. 이 교재는 FIS EKS Pod 실험을 실행했다고 주장하지 않습니다.

1. 복구하는 상태

체크포인트는 물체를 놓고 모든 상자의 지지·정지 상태를 확인한 action 경계에만 저장합니다. 요청 hash, 완료 action 수, 로봇·물체 상태, 이미 기록한 프레임과 물리 근거가 포함됩니다.

새 worker는 같은 요청의 체크포인트를 검증하고 장면의 초기 상태로 한 번 복원한 뒤 실제 PhysX로 접촉을 다시 구성합니다. 완료 action은 건너뛰고 나머지를 실행합니다. 잡는 도중의 접촉·solver·CUDA 내부 상태를 비트 단위로 이어가는 방식은 아닙니다.

S3의 시도별 불변 경로와 DynamoDB의 worker lease·조건부 갱신이 함께 필요합니다. S3 업로드만 성공해도 취소되거나 lease를 잃은 worker가 진행·성공 상태를 덮어쓸 수 없어야 합니다.

2. 명시적 운영자 시험 생성

운영자 터미널의 isaac, ik, isaac_wait_run 함수를 준비합니다. 최소 두 action이 필요하며 일반 작업은 recover가 거부합니다.

이 예시는 기본 standard Spot·warm 0에서 수행합니다. 17장의 holder를 켰다면 먼저 isaac warm --replicas 0으로 중지하고 비교 조건을 기록합니다. --recovery-test와 On-Demand를 결합하는 요청은 CLI가 거부합니다.

터미널bash
VSCode Server 터미널에서 실행
set -euo pipefail
cd "$ISAAC_REPO"
isaac submit --scenario stack --seed 271 --order blue,red,orange --recovery-test \
  > "$ISAAC_EVIDENCE/recovery-submit.json"
export ISAAC_RECOVERY_ID="$(jq -er '.id' "$ISAAC_EVIDENCE/recovery-submit.json")"
jq -e '.owner == "operator-validation" and .operatorRecoveryTest == true
       and .operatorTestHoldSeconds == 30 and (.plan.actions | length) >= 2' \
  "$ISAAC_EVIDENCE/recovery-submit.json"

시험 옵션은 성공한 비최종 체크포인트 뒤에만 30초 정지 구간을 만듭니다. 초기 0-action 체크포인트, 잡는 도중, 마지막 action 뒤에는 정지하지 않습니다. 이 옵션은 웹 요청 필드가 아닙니다.

3. 커밋된 비최종 체크포인트 대기

다음 대기는 최대 25분이며 성공·실패를 먼저 만나면 중단합니다. 대기 시간을 GPU 복구 성능 수치로 기록하지 않습니다.

터미널bash
VSCode Server 터미널에서 실행
ISAAC_CHECKPOINT_READY=0
for ((i=0; i<750; i++)); do
  isaac run --id "$ISAAC_RECOVERY_ID" > "$ISAAC_EVIDENCE/recovery-before.json"
  if jq -e '(.state == "queued" or .state == "starting" or .state == "running")
      and (.checkpointCompleted // 0) >= 1
      and .checkpointCompleted < (.plan.actions | length)' \
      "$ISAAC_EVIDENCE/recovery-before.json" >/dev/null; then
    ISAAC_CHECKPOINT_READY=1
    break
  fi
  case "$(jq -er '.state' "$ISAAC_EVIDENCE/recovery-before.json")" in
    succeeded|failed|cancelled) break ;;
  esac
  sleep 2
done
test "$ISAAC_CHECKPOINT_READY" -eq 1
jq '{id,state,attempts,workerId,checkpointCompleted,checkpointKey,gpu,readyEvidence}' \
  "$ISAAC_EVIDENCE/recovery-before.json"

test가 실패하면 교체하지 않습니다. 이미 완료됐거나 실패한 작업을 회수 시험으로 바꾸지 않습니다.

4. 해당 시험의 NodeClaim·Pod만 교체

터미널bash
VSCode Server 터미널에서 실행
isaac recover --id "$ISAAC_RECOVERY_ID"
cp ".local/isaac/results/$ISAAC_RECOVERY_ID-recovery.json" \
  "$ISAAC_EVIDENCE/recovery-request.json"
jq '{kind,actualSpotInterruptionInjected,runId,checkpointCompleted,
     oldNode,oldNodeClaim,oldPod,oldWorkerId,requestedAt}' \
  "$ISAAC_EVIDENCE/recovery-request.json"

이 명령은 실제 Kubernetes 변경입니다. 실행기는 operator-validation, operatorRecoveryTest, 비최종 진행, 현재 fenced worker UID, 전용 Spot pool, 다른 일반 workload가 없는 노드, 유일한 NodeClaim을 확인합니다. 그 후 해당 NodeClaim과 해당 Pod를 삭제합니다. namespace 전체의 PDB를 완화하거나 다른 노드의 Pod를 삭제하지 않습니다.

이미 교체를 요청한 시험의 기록 파일이 있으면 재요청을 거부합니다. 기록 파일을 지워 가드를 우회하지 않습니다. 보존된 checkpointKey는 교체 시점의 정확한 복구 근거입니다. 나중의 최종 checkpoint와 혼동하지 않도록 그 키의 실제 파일을 따로 보관합니다.

터미널bash
VSCode Server 터미널에서 실행
"$ISAAC_PYTHON" - <<'PY'
import json, os
from pathlib import Path
from infra.isaac import deployment as d
from isaac.cache import download
from isaac.state import artifact_key
cfg = d.config()
aws = d.session(cfg)
evidence = Path(os.environ["ISAAC_EVIDENCE"])
request = json.loads((evidence / "recovery-request.json").read_text())
run_id = os.environ["ISAAC_RECOVERY_ID"]
assert request["runId"] == run_id and request["actualSpotInterruptionInjected"] is False
key = artifact_key(run_id, request["checkpointKey"])
path = evidence / "recovery-boundary.json"
digest = download(aws.client("s3"), d.outputs(aws, cfg)["ArtifactBucket"], key, path, 32 * 1024 * 1024)
checkpoint = json.loads(path.read_text())
assert checkpoint["completedActions"] == request["checkpointCompleted"]
assert all(o["released"] and o["supported"] and o["stable"]
           for o in checkpoint["boundary"]["objects"].values())
print(json.dumps({"path": str(path), "sha256": digest,
                  "completedActions": checkpoint["completedActions"]}))
PY

5. 새 worker의 실제 재개 확인

터미널bash
VSCode Server 터미널에서 실행
isaac_wait_run "$ISAAC_RECOVERY_ID" 1800
isaac run --id "$ISAAC_RECOVERY_ID" > "$ISAAC_EVIDENCE/recovery-after.json"
isaac artifact --id "$ISAAC_RECOVERY_ID" --kind replay \
  > "$ISAAC_EVIDENCE/recovery-replay-file.json"
isaac artifact --id "$ISAAC_RECOVERY_ID" --kind checkpoint \
  > "$ISAAC_EVIDENCE/recovery-checkpoint-file.json"
isaac artifact --id "$ISAAC_RECOVERY_ID" --kind preview \
  > "$ISAAC_EVIDENCE/recovery-preview-file.json"

다음 검사는 외부 실행 ID와 물리 checkpoint의 내부 ID를 각각 비교합니다. 값을 편집해 서로 같게 만들지 않습니다.

터미널bash
VSCode Server 터미널에서 실행
"$ISAAC_PYTHON" - <<'PY'
from datetime import datetime
import json, os
from pathlib import Path
folder = Path(os.environ["ISAAC_EVIDENCE"])
load = lambda name: json.loads((folder / name).read_text())
before, after = load("recovery-before.json"), load("recovery-after.json")
requested, boundary = load("recovery-request.json"), load("recovery-boundary.json")
replay = json.loads(Path(load("recovery-replay-file.json")["path"]).read_text())
checkpoint = json.loads(Path(load("recovery-checkpoint-file.json")["path"]).read_text())
run_id = os.environ["ISAAC_RECOVERY_ID"]
assert before["id"] == after["id"] == requested["runId"] == replay["id"] == run_id
assert after["state"] == replay["status"] == "succeeded"
assert after["attempts"] > before["attempts"]
assert after["workerId"] != requested["oldWorkerId"]
assert after["gpu"]["node"] != requested["oldNode"]
assert after["readyEvidence"]["resumeCount"] >= 1
assert boundary["runId"] == checkpoint["runId"] == after["readyEvidence"]["runId"]
assert boundary["requestHash"] == checkpoint["requestHash"] == replay["provenance"]["requestHash"]
assert checkpoint["completedActions"] == len(after["plan"]["actions"])
assert replay["frames"][:len(boundary["frames"])] == boundary["frames"]
assert all(b["t"] > a["t"] for a, b in zip(replay["frames"], replay["frames"][1:]))
assert [s["objectId"] for s in replay["steps"]] == ["blue", "red", "orange"]
assert replay["metrics"]["success"] and replay["metrics"]["placed"] == 3
assert all(c["passed"] for c in replay["checks"])
assert any(c["name"] == "restored released milestone" and c["passed"] for c in replay["checks"])
ready_at = datetime.fromisoformat(after["readyEvidence"]["timestamp"].replace("Z", "+00:00")).timestamp()
seconds = ready_at - requested["requestedAt"]
assert seconds >= 0
report = {
    "runId": run_id, "oldNode": requested["oldNode"], "newNode": after["gpu"]["node"],
    "checkpointCompletedBeforeReplacement": boundary["completedActions"],
    "attempts": after["attempts"], "replacementRequestToRestoredReadySeconds": round(seconds, 3),
    "actualSpotInterruptionInjected": False, "passed": True,
}
(folder / "recovery-verification.json").write_text(json.dumps(report, indent=2) + "\n")
print(json.dumps(report, indent=2))
PY

기록된 시간은 명시적 교체 요청부터 복원 후 RTX Ready까지입니다. 실제 Spot 회수 통보부터의 RTO, 장애 없는 세션 유지, 모든 AZ에서 보장되는 시작 시간으로 일반화하지 않습니다.

회수에 대비한 운영 판단

Spot 회수 알림만 기다려 마지막 순간에 상태를 저장하지 않습니다. 정상 동작 중의 안정된 경계 저장이 복구의 기본입니다. 마지막 경계 이후 진행 중이던 action은 다시 실행할 수 있으므로 비가역 외부 효과가 있는 작업에 그대로 확대하지 않습니다.

WhenEmpty·PDB·긴 NodePool 유예는 실제 Spot 인스턴스의 회수 시간을 늘려 주지 않습니다. Auto Mode의 노드 만료·drift도 복구 사유가 될 수 있습니다. 이 리허설을 실제 회수 이벤트 검증과 분리해 운영 기록에 남깁니다.

실패 시 재개

증상 판단
체크포인트 0 또는 최종 수 교체 시험 조건 아님. 현재 작업을 지우지 않음
일반 사용자·일반 operator 실행 recover 거부가 정상
다른 workload가 같은 노드에 있음 이웃 보호에 따라 중단. 강제 drain으로 우회하지 않음
재개 Pod가 lease 대기 이전 worker의 최대 45초 lease와 heartbeat 확인
새 Spot 용량 없음 Pending·NodeClaim 이벤트·AZ·후보 가용성 확인. 자동 On-Demand 전환 없음
checkpoint hash/경계 거부 요청·물리 설정·씬 불일치 조사. 체크포인트 편집 금지
프레임 prefix·시간·지지 검사 실패 성공이 아닌 복구 결함으로 기록

완료 체크리스트

  • 명시적 operator 시험과 커밋된 비최종 경계를 사용했습니다.
  • 전용 NodeClaim·Pod만 교체했고 이웃 workload 가드를 유지했습니다.
  • 외부 실행 ID, 내부 물리 ID, 프레임 prefix, 재개 횟수와 최종 물리를 확인했습니다.
  • 실제 새 노드에서 RTX Ready를 확인했습니다.
  • 실제 Spot/FIS 주입이 아닌 리허설로 기록했습니다.

AI CLI 카드: 16. 복구 리허설 · 다음: 17. GPU 운영

AI CLI 프롬프트 카드

아래 프롬프트를 복사해 실습 EC2에서 선택한 Codex, Kiro CLI, Claude Code의 대화 입력창에 붙여넣습니다. Markdown 카드 다운로드

Codex프롬프트 입력

선택한 AI CLI의 대화 입력창에 붙여넣으세요. 세 도구가 같은 프롬프트를 사용합니다.

Robot Atlas 16장 복구 리허설을 수행하세요.
ISAAC_REPO/workshop/chapters/16-spot-recovery.md,
infra/isaac/operations.py의 recover와 isaac/state.py의 lease 계약을 먼저 읽으세요.

절차:
1. 실제 GPU 성공을 확인한 환경에서 --recovery-test로 2개 이상 action의 운영자 작업을 만듭니다.
2. owner=operator-validation, operatorRecoveryTest=true를 확인합니다.
3. checkpointCompleted >= 1이고 전체 action 수보다 작은 커밋 경계를 기다립니다.
4. recover의 현재 worker UID·전용 Spot pool·단일 NodeClaim·이웃 workload 검사를 유지합니다.
5. 해당 시험의 NodeClaim과 Pod만 교체하고 저장된 recovery JSON을 읽습니다.
6. 교체 시점의 정확한 checkpointKey 파일을 보관하고 새 노드의 재개를 검증합니다.

성공 근거:
외부 run ID 유지, 시도/worker UID 변화, 다른 노드, 내부 물리 ID/hash 유지,
기존 프레임 prefix 보존과 증가하는 시간, 완료 action 건너뛰기,
복원 후 RTX Ready·지지/안정성 재검사·최종 물리 성공이 모두 필요합니다.
취소·lease 상실 이후 늦은 worker가 성공으로 덮어쓰지 않아야 합니다.
실패하면 상태·산출물·원인을 보존하고 결과를 수정하지 마세요.

시험 범위:
이 recover는 직접 NodeClaim·Pod 교체이며 FIS API나 실제 Spot 통보를 주입하지 않습니다.
actualSpotInterruptionInjected=false를 유지하세요.
Auto Mode는 FIS ec2:SendSpotInstanceInterruptions 등 일부 EC2 작업을 지원하지 않지만
FIS EKS Pod 작업은 지원합니다. 모든 FIS/중단 시험이 불가능하다고 말하지 마세요.
지원되는 FIS Pod 실험도 이 리허설에서 실행한 것으로 간주하지 마세요.

보호:
일반 사용자/일반 operator 작업, 최종·0-action checkpoint, 이웃 workload가 있으면 중단합니다.
PDB·finalizer·소유권 가드를 풀거나 전체 namespace/클러스터를 drain하지 마세요.
기존 GPU01·Jeju·Fargate와 공유 discovery/visibility 설정을 변경하지 마세요.

교체 요청→복원 Ready의 실제 시간과 계측 범위를 보고하세요. 보장 RTO를 만들지 마세요.

읽기 기록

읽음 표시는 이 브라우저에만 저장됩니다.
실제 실습 결과는 터미널에서 확인하세요.