18진행자·운영자 Isaac Sim · EKS예상 약 20분, 별도 일정
전용 GPU 환경 정리
GPU 작업과 노드를 먼저 정리한 뒤 전용 스택과 보존 데이터를 확인합니다.
진행자·운영자 선택 트랙, 예상 약 20분입니다. AWS 비동기 삭제는 더 걸릴 수 있습니다. 이 장은 공용 GPU 환경을 운영하는 진행자만 실행합니다.
대상은 gpu-cluster-01과 RobotAtlasIsaac, RobotAtlasIsaacAddons, RobotAtlasIsaacBuild입니다. 기존 GPU01, Jeju, 운영 Fargate, 공유 VPC·Subnet·NAT·인증서·DNS는 삭제하거나 중지하지 않습니다. 현재 isaac.py에는 cleanup 명령이 없으므로 아래에 소유권 검사와 명시적 Kubernetes·CloudFormation 작업을 제공합니다.
1. 신규 접수 중지와 삭제 전 목록
진행자는 새 운영자 제출을 중지하고 웹의 Isaac 기능도 비활성화된 상태인지 확인합니다. 웹이 활성화되어 있다면 정상적인 앱 변경 검토 절차로 Isaac 경로만 비활성화한 뒤 재개합니다. Fargate 서비스를 중지하는 방식으로 해결하지 않습니다.
아래 조회는 실제 Fargate의 현재·실행 중 task definition에서 Isaac 환경값이 비어 있는지 확인합니다. 로컬 isaacEnabled: false만 보고 원격도 꺼졌다고 가정하지 않습니다.
set -euo pipefail
cd "$ISAAC_REPO"
export ISAAC_CLEANUP_DIR="$ISAAC_EVIDENCE/cleanup-$(date -u +%Y%m%dT%H%M%SZ)"
mkdir -p "$ISAAC_CLEANUP_DIR"
"$ISAAC_PYTHON" - <<'PY'
import hashlib, json, os
from pathlib import Path
import yaml
from infra.isaac import deployment as d
from isaac.state import ACTIVE
cfg = d.config()
aws = d.session(cfg)
cf = aws.client("cloudformation")
out = d.outputs(aws, cfg)
web = cf.describe_stacks(StackName="RobotAtlasApp")["Stacks"][0]
tags = {t["Key"]: t["Value"] for t in web.get("Tags", [])}
assert tags.get("Project") == "robot-atlas" and tags.get("DeploymentOwner") == "RobotAtlas"
web_out = {o["OutputKey"]: o["OutputValue"] for o in web["Outputs"]}
ecs = aws.client("ecs")
service = ecs.describe_services(
cluster=web_out["ClusterName"], services=[web_out["ServiceName"]])["services"][0]
assert service["runningCount"] >= 1, "Keep the existing Fargate service running"
definitions = {service["taskDefinition"]}
for page in ecs.get_paginator("list_tasks").paginate(
cluster=web_out["ClusterName"], serviceName=web_out["ServiceName"], desiredStatus="RUNNING"):
arns = page["taskArns"]
for offset in range(0, len(arns), 100):
result = ecs.describe_tasks(cluster=web_out["ClusterName"], tasks=arns[offset:offset + 100])
assert not result.get("failures"), "Could not verify every running web task"
definitions.update(t["taskDefinitionArn"] for t in result["tasks"])
for arn in definitions:
task = ecs.describe_task_definition(taskDefinition=arn)["taskDefinition"]
container = next(c for c in task["containerDefinitions"] if c["name"] == "web")
env = {e["name"]: e["value"] for e in container.get("environment", [])}
assert not env.get("ISAAC_JOBS_TABLE") and not env.get("ISAAC_ARTIFACT_BUCKET"), \
"Disable only the Isaac web integration through the reviewed app release process first"
assert not {"ISAAC_JOBS_TABLE", "ISAAC_ARTIFACT_BUCKET"}.intersection(
s["name"] for s in container.get("secrets", [])), "Isaac integration may be enabled by a secret"
table = aws.resource("dynamodb").Table(out["JobsTable"])
arguments, active = {}, []
while True:
page = table.scan(**arguments)
active.extend(r["id"] for r in page["Items"] if r.get("state") in ACTIVE)
if not page.get("LastEvaluatedKey"):
break
arguments["ExclusiveStartKey"] = page["LastEvaluatedKey"]
assert not active, "Wait for or cancel active GPU jobs through their existing owner before cleanup"
class Loader(yaml.SafeLoader):
pass
def tag(loader, suffix, node):
if isinstance(node, yaml.ScalarNode): return loader.construct_scalar(node)
if isinstance(node, yaml.SequenceNode): return loader.construct_sequence(node)
return loader.construct_mapping(node)
Loader.add_multi_constructor("!", tag)
inventory = {"clusterName": cfg["clusterName"], "accountId": cfg["accountId"],
"webCluster": web_out["ClusterName"], "webService": web_out["ServiceName"], "stacks": {}}
for component in ("cluster", "addons", "build"):
name = d.stack_name(cfg, component)
stack = d.owned_stack(cf, name)
if not stack:
continue
assert not stack["StackStatus"].endswith("_IN_PROGRESS")
template = cf.get_template(StackName=stack["StackId"], TemplateStage="Original")["TemplateBody"]
raw = template if isinstance(template, str) else json.dumps(template, sort_keys=True, default=str)
template_sha256 = hashlib.sha256(raw.encode()).hexdigest()
if isinstance(template, str):
template = yaml.load(template, Loader=Loader)
resources = [r for p in cf.get_paginator("list_stack_resources").paginate(StackName=stack["StackId"])
for r in p["StackResourceSummaries"]]
assert not any(r["ResourceType"].startswith(("AWS::Route53::", "AWS::ECS::"))
for r in resources), "Unexpected shared/DNS resource in the GPU stack"
retained = []
for resource in resources:
policy = template["Resources"][resource["LogicalResourceId"]].get("DeletionPolicy")
if resource["ResourceType"] in {"AWS::S3::Bucket", "AWS::DynamoDB::Table", "AWS::ECR::Repository"}:
assert policy == "Retain", "GPU data/artifacts must remain retained"
if policy == "Retain":
retained.append({k: resource.get(k) for k in
("LogicalResourceId", "ResourceType", "PhysicalResourceId")})
inventory["stacks"][component] = {
"name": name, "stackId": stack["StackId"], "templateSha256": template_sha256,
"resources": [{k: r.get(k) for k in ("LogicalResourceId", "ResourceType", "PhysicalResourceId")}
for r in resources], "retained": retained,
}
path = Path(os.environ["ISAAC_CLEANUP_DIR"]) / "inventory.json"
with path.open("x") as stream:
json.dump(inventory, stream, indent=2)
print(json.dumps({"inventory": str(path), "activeJobs": 0,
"webStillRunning": True, "webIsaacDisabled": True}, indent=2))
PY
jq '.stacks | map_values({name,stackId,retained})' "$ISAAC_CLEANUP_DIR/inventory.json"
활성 작업이 있으면 현재 소유자의 취소 또는 완료·deadline 처리를 먼저 확인합니다. 테이블의 state를 수동으로 성공/취소로 덮어쓰거나 admission 항목만 지워 통과시키지 않습니다. 재개할 때는 처음 저장한 ISAAC_CLEANUP_DIR과 inventory를 유지합니다.
2. warm·컨트롤러 중지와 소유 Job 제거
17장의 선택 holder가 있다면 먼저 중지합니다. 기본 warm 0을 유지한 환경에서는 없는 holder를 새로 만들지 않습니다. namespace quota 2는 Spot·On-Demand·holder GPU 요청의 합계에 적용되므로 두 pool을 각각 확인합니다.
isaac warm --replicas 0
다음은 실제 Kubernetes 변경입니다. 소유 warm Pod 종료를 기다리고, 전용 namespace의 알 수 없는 Job이나 실행 중 GPU worker가 있으면 중단합니다. 신규 제출을 막은 상태에서 컨트롤러를 0으로 줄이고 완료된 전용 Job을 정리합니다.
"$ISAAC_PYTHON" - <<'PY'
import json, subprocess
from infra.isaac import deployment as d
from isaac.state import JOB_ID
cfg = d.config()
aws = d.session(cfg)
kc = d.kubeconfig(aws, cfg)
base = ["kubectl", "--kubeconfig", str(kc), "--context", "gpu-cluster-01"]
ns = cfg["namespace"]
def get(*args):
return json.loads(subprocess.check_output(base + list(args) + ["-o", "json"], text=True))
namespace = get("get", "namespace", ns)
assert namespace["metadata"]["labels"].get("app.kubernetes.io/part-of") == "robot-atlas"
holders = get("-n", ns, "get", "pods", "-l", "app=isaac-warm")["items"]
for holder in holders:
assert holder["metadata"].get("labels", {}).get("app.kubernetes.io/part-of") == "robot-atlas"
if holders:
subprocess.run(base + ["-n", ns, "wait", "--for=delete", "pod",
"-l", "app=isaac-warm", "--timeout=120s"], check=True)
jobs = get("-n", ns, "get", "jobs")["items"]
for job in jobs:
name, labels = job["metadata"]["name"], job["metadata"].get("labels", {})
assert labels.get("app.kubernetes.io/part-of") == "robot-atlas"
assert (JOB_ID.fullmatch(name) and labels.get("robot-atlas/run") == name) or \
(name == "isaac-gpu-probe" and labels.get("app") == "isaac-probe")
pods = get("-n", ns, "get", "pods")["items"]
for pod in pods:
if pod.get("status", {}).get("phase") in ("Succeeded", "Failed"):
continue
assert pod["metadata"].get("labels", {}).get("app") == "isaac-controller", \
"A live GPU/unknown pod remains; wait before deleting jobs"
deployments = get("-n", ns, "get", "deployments")["items"]
for deployment in deployments:
meta = deployment["metadata"]
assert meta["name"] in ("isaac-controller", "isaac-warm")
assert meta.get("labels", {}).get("app.kubernetes.io/part-of") == "robot-atlas"
if meta["name"] == "isaac-warm":
assert deployment["spec"].get("replicas") == 0, "Stop the owned warm holder first"
else:
subprocess.run(base + ["-n", ns, "scale", "deployment/isaac-controller", "--replicas=0"], check=True)
remaining = get("-n", ns, "get", "pods", "-l", "app=isaac-controller")["items"]
if remaining:
subprocess.run(base + ["-n", ns, "wait", "--for=delete", "pod",
"-l", "app=isaac-controller", "--timeout=120s"], check=True)
for job in jobs:
subprocess.run(base + ["-n", ns, "delete", "job", job["metadata"]["name"],
"--wait=true", "--timeout=120s"], check=True)
print(json.dumps({"controllerReplicas": 0, "deletedOwnedJobs": [j["metadata"]["name"] for j in jobs]}))
PY
workloads, warm --replicas 1, enable-ondemand, 새 submit을 정리 중에 실행하지 않습니다. 위 명령은 Fargate 서비스나 다른 클러스터의 컨트롤러를 중지하지 않습니다.
3. 노드 삭제용 소유권 검사 함수
아래 함수는 문서의 셸 도움 함수입니다. 기존 CLI에 없는 명령을 가장하지 않습니다. 허용한 Spot·선택 On-Demand·CPU pool 이름만 받으며 각 NodeClass 참조·라벨과 노드의 이웃 workload를 확인합니다.
isaac_delete_pool() {
"$ISAAC_PYTHON" - "$1" <<'PY'
import json, os, subprocess, sys
from pathlib import Path
from infra.isaac import deployment as d
pool = sys.argv[1]
classes = {"isaac-sim-spot": "isaac-sim-private", "isaac-sim-ondemand": "isaac-sim-private",
"robot-atlas-cpu": "robot-atlas-cpu"}
assert pool in classes, "Only the explicitly named owned pools can be removed"
cfg = d.config()
aws = d.session(cfg)
kc = d.kubeconfig(aws, cfg)
base = ["kubectl", "--kubeconfig", str(kc), "--context", "gpu-cluster-01"]
def get(*args):
text = subprocess.check_output(base + list(args) + ["-o", "json"], text=True)
return json.loads(text) if text.strip() else None
item = get("get", "nodepool", pool, "--ignore-not-found")
if item:
assert item["metadata"].get("labels", {}).get("app.kubernetes.io/part-of") == "robot-atlas"
assert item["spec"]["template"]["spec"]["nodeClassRef"]["name"] == classes[pool]
claims = get("get", "nodeclaims", "-l", "karpenter.sh/nodepool=" + pool)["items"]
for claim in claims:
assert claim["spec"]["nodeClassRef"]["name"] == classes[pool]
node = claim.get("status", {}).get("nodeName")
if not node:
continue
pods = get("get", "pods", "-A", "--field-selector", "spec.nodeName=" + node)["items"]
for pod in pods:
if pod.get("status", {}).get("phase") in ("Succeeded", "Failed"):
continue
assert any(r.get("kind") == "DaemonSet" for r in pod["metadata"].get("ownerReferences", [])), \
"Another live workload is on this node; refusing deletion"
evidence = [{"name": c["metadata"]["name"], "node": c.get("status", {}).get("nodeName"),
"providerID": c.get("status", {}).get("providerID")} for c in claims]
path = Path(os.environ["ISAAC_CLEANUP_DIR"]) / (pool + "-nodeclaims.json")
if not path.exists():
d.write_json(path, evidence)
print(json.dumps({"pool": pool, "nodeclaims": evidence}, indent=2))
if item:
subprocess.run(base + ["delete", "nodepool", pool, "--wait=false"], check=True)
PY
}
먼저 두 GPU pool을 확인·제거하고 실제 NodeClaim이 사라지는지 확인합니다. On-Demand를 활성화하지 않았다면 해당 조회·제거는 빈 결과로 끝납니다. 임의의 노드 이름이나 kubectl delete nodes --all을 사용하지 않습니다.
isaac_delete_pool isaac-sim-spot
isaac_delete_pool isaac-sim-ondemand
export ISAAC_KUBECONFIG="$(isaac kubeconfig)"
for ISAAC_POOL in isaac-sim-spot isaac-sim-ondemand; do
for ((i=0; i<120; i++)); do
ISAAC_REMAINING="$(ik get nodeclaims -l "karpenter.sh/nodepool=$ISAAC_POOL" -o json | jq '.items | length')"
[ "$ISAAC_REMAINING" -eq 0 ] && break
sleep 5
done
test "$ISAAC_REMAINING" -eq 0
ik get nodes -l "karpenter.sh/nodepool=$ISAAC_POOL"
done
노드 삭제가 지연되면 finalizer·이벤트와 남은 workload를 조사합니다. 보호 설정이나 finalizer를 제거해 강제 통과하지 않습니다. 보관된 providerID로 해당 EC2 인스턴스 종료도 확인합니다.
4. 전용 build·addons 스택 삭제
CodeBuild 작업이 진행 중이면 완료 또는 그 전용 프로젝트의 해당 빌드만 중지한 결과를 먼저 확인합니다. 빌드가 계속 실행 중인 상태에서 프로젝트를 삭제하지 않습니다.
다음 함수는 inventory에 저장한 정확한 stack ID, 현재 태그·자원 목록을 다시 대조합니다. 세 개의 허용 component 외에는 받지 않습니다. 첫 호출 전 출력될 삭제 대상과 보존 목록을 검토합니다.
isaac_delete_stack() {
"$ISAAC_PYTHON" - "$1" <<'PY'
import hashlib, json, os, sys
from pathlib import Path
from infra.isaac import deployment as d
component = sys.argv[1]
assert component in ("build", "addons", "cluster")
cfg = d.config()
aws = d.session(cfg)
cf = aws.client("cloudformation")
inventory = json.loads((Path(os.environ["ISAAC_CLEANUP_DIR"]) / "inventory.json").read_text())
assert inventory["clusterName"] == cfg["clusterName"] == "gpu-cluster-01"
assert inventory["accountId"] == cfg["accountId"]
record = inventory["stacks"].get(component)
current = d.owned_stack(cf, d.stack_name(cfg, component))
if current is None:
print(json.dumps({"component": component, "status": "NOT_CREATED"}))
raise SystemExit(0)
assert record and current["StackId"] == record["stackId"]
assert not current["StackStatus"].endswith("_IN_PROGRESS")
template = cf.get_template(StackName=current["StackId"], TemplateStage="Original")["TemplateBody"]
raw = template if isinstance(template, str) else json.dumps(template, sort_keys=True, default=str)
assert hashlib.sha256(raw.encode()).hexdigest() == record["templateSha256"], \
"Template or retention policy changed after inventory"
resources = [{k: r.get(k) for k in ("LogicalResourceId", "ResourceType", "PhysicalResourceId")}
for p in cf.get_paginator("list_stack_resources").paginate(StackName=current["StackId"])
for r in p["StackResourceSummaries"]]
sort = lambda values: sorted(values, key=lambda r: r["LogicalResourceId"])
assert sort(resources) == sort(record["resources"]), "Owned resources changed since inventory"
if component == "build":
project = next(r["PhysicalResourceId"] for r in resources if r["ResourceType"] == "AWS::CodeBuild::Project")
assert project == "robot-atlas-isaac-build"
builds = aws.client("codebuild")
ids, token = [], None
while True:
page = builds.list_builds_for_project(projectName=project, **({"nextToken": token} if token else {}))
ids.extend(page["ids"])
token = page.get("nextToken")
if not token: break
for offset in range(0, len(ids), 100):
rows = builds.batch_get_builds(ids=ids[offset:offset + 100])["builds"]
assert all(r.get("buildComplete") is True for r in rows), "An owned build is still active"
if component == "cluster":
from isaac.state import ACTIVE
table = aws.resource("dynamodb").Table(d.outputs(aws, cfg)["JobsTable"])
arguments = {}
while True:
page = table.scan(**arguments)
assert not any(r.get("state") in ACTIVE for r in page["Items"]), "A new active job appeared"
if not page.get("LastEvaluatedKey"): break
arguments["ExclusiveStartKey"] = page["LastEvaluatedKey"]
for dependent in ("build", "addons"):
assert d.owned_stack(cf, d.stack_name(cfg, dependent)) is None
kc = d.kubeconfig(aws, cfg)
import subprocess
base = ["kubectl", "--kubeconfig", str(kc), "--context", "gpu-cluster-01"]
for resource in ("nodeclaims", "nodes"):
rows = json.loads(subprocess.check_output(base + ["get", resource, "-o", "json"], text=True))
assert not rows["items"], "Stop nodes before deleting the dedicated cluster"
jobs = json.loads(subprocess.check_output(base + ["get", "jobs", "-A", "-o", "json"], text=True))
assert not jobs["items"], "A job remains in the cluster"
# Managed EC2 resources may be hidden from list APIs. Query captured IDs only.
import re
ids = set()
for pool in ("isaac-sim-spot", "isaac-sim-ondemand", "robot-atlas-cpu"):
path = Path(os.environ["ISAAC_CLEANUP_DIR"]) / (pool + "-nodeclaims.json")
for claim in json.loads(path.read_text()):
provider = claim.get("providerID")
if not provider:
assert not claim.get("node"), "A node had no captured providerID; inspect it before teardown"
continue
instance_id = provider.rsplit("/", 1)[-1]
assert re.fullmatch(r"i-(?:[0-9a-f]{8}|[0-9a-f]{17})", instance_id)
ids.add(instance_id)
for instance_id in sorted(ids):
try:
result = aws.client("ec2").describe_instances(InstanceIds=[instance_id])
except Exception as error:
if getattr(error, "response", {}).get("Error", {}).get("Code") == "InvalidInstanceID.NotFound":
continue
raise
instances = [i for reservation in result["Reservations"] for i in reservation["Instances"]]
assert instances and all(i["InstanceId"] == instance_id and i["State"]["Name"] == "terminated"
for i in instances), "A captured instance has not terminated yet"
print(json.dumps({"deleting": current["StackName"], "stackId": current["StackId"],
"retained": record["retained"]}, indent=2))
cf.delete_stack(StackName=current["StackId"])
cf.get_waiter("stack_delete_complete").wait(
StackName=current["StackId"], WaiterConfig={"Delay": 10, "MaxAttempts": 180})
print(json.dumps({"component": component, "status": "DELETE_COMPLETE"}))
PY
}
isaac_delete_stack build
isaac_delete_stack addons
Addons 삭제로 CoreDNS가 제거된 뒤 CPU pool을 정리합니다. CoreDNS를 실행 중인 상태에서 CPU 노드를 강제로 제거할 필요가 없습니다.
isaac_delete_pool robot-atlas-cpu
for ((i=0; i<120; i++)); do
ISAAC_REMAINING="$(ik get nodeclaims -o json | jq '.items | length')"
[ "$ISAAC_REMAINING" -eq 0 ] && break
sleep 5
done
test "$ISAAC_REMAINING" -eq 0
test "$(ik get nodes -o json | jq '.items | length')" -eq 0
isaac_delete_stack cluster
다른 pool·Job·workload가 있으면 마지막 검사가 거부합니다. 그 자원까지 묶어 지우지 않고 소유자를 확인합니다.
5. 삭제 후와 보존 데이터
isaac status > "$ISAAC_CLEANUP_DIR/stacks-after.json"
jq -e '.cluster == "NOT_CREATED" and .addons == "NOT_CREATED" and .build == "NOT_CREATED"' \
"$ISAAC_CLEANUP_DIR/stacks-after.json"
"$ISAAC_PYTHON" - <<'PY'
import json, os
from pathlib import Path
from infra.isaac import deployment as d
cfg = d.config()
aws = d.session(cfg)
inventory = json.loads((Path(os.environ["ISAAC_CLEANUP_DIR"]) / "inventory.json").read_text())
assert inventory["accountId"] == cfg["accountId"]
result = aws.client("ecs").describe_services(
cluster=inventory["webCluster"], services=[inventory["webService"]])
assert not result.get("failures") and len(result["services"]) == 1
service = result["services"][0]
assert service["status"] == "ACTIVE" and service["runningCount"] >= 1
print(json.dumps({"existingFargateRunning": True, "running": service["runningCount"]}))
PY
| 남을 수 있는 자원 | 확인할 것 |
|---|---|
| S3 | 작업 결과·체크포인트·캐시·빌드 소스와 모든 버전 |
| DynamoDB | 실행·admission·TTL·백업/PITR |
| ECR | 원본·보강 base·worker·controller 이미지 |
| 로그 | cluster/worker의 Retain 로그. build 로그의 삭제 정책은 별도 |
| EC2/EBS | 저장한 NodeClaim/providerID의 실제 종료·볼륨 잔여 |
데이터는 Retain하며 이 과정에서 버킷 비우기·테이블/이미지 삭제를 수행하지 않습니다. 동일한 이름으로 재생성하면 보존 자원과 충돌할 수 있으므로 별도 가져오기/정리 검토가 필요합니다.
EC2 기본 목록에 없다는 이유로 종료됐다고 기록하지 않습니다. 관리형 자원 visibility는 계정 전체 설정이므로 바꾸지 않고, 저장한 NodeClaim/providerID의 특정 instance ID를 조회합니다. 기존 GPU01과 공유 discovery 태그도 수정하지 않습니다.
기존 Fargate가 계속 실행 중인지 확인하고, Isaac 권한용 새 정책 제거를 Fargate 서비스 삭제와 혼동하지 않습니다. DNS 등록·삭제 명령은 없습니다. 기존 GPU01·Jeju·공유 네트워크도 유지합니다. CPU/GPU 노드가 사라져도 보존 저장소·로그 비용이 남을 수 있습니다.
실패 시 재개
| 증상 | 다음 단계 |
|---|---|
| 웹에서 Isaac가 활성 | 검토된 앱 절차로 Isaac만 비활성화 후 재개 |
| active Job·실행 중 GPU Pod | 소유자의 취소·완료·deadline 처리와 종료 확인 |
| 이웃 workload 거부 | 다른 작업을 지우지 않고 소유자와 범위 확인 |
| CodeBuild active | 전용 빌드의 완료/중지 결과 확인 |
| NodeClaim/finalizer 잔여 | 이벤트·PDB·실제 인스턴스 상태 조사 |
| stack ID·자원 목록 불일치 | 기존 inventory를 보존하고 변경을 다시 검토 |
| CF 삭제 진행 중/실패 | 실제 상태·이벤트를 조회. 전체 절차를 중복 실행하지 않음 |
완료 체크리스트
- 신규 접수와 실제 웹 Isaac 설정을 확인했습니다.
- warm·진행 중 작업·빌드를 종료하고 컨트롤러·Job·Spot/On-Demand·CPU 노드를 순서대로 정리했습니다.
- 정확한 소유 스택만 삭제하고 상태를 재조회했습니다.
- 데이터 Retain 목록과 실제 EC2/EBS 잔여를 기록했습니다.
- 기존 GPU01·Jeju·운영 Fargate·공유 네트워크·DNS를 유지했습니다.
AI CLI 카드: 18. 전용 환경 정리 · 참고: 운영자 범위
AI CLI 프롬프트 카드
아래 프롬프트를 복사해 실습 EC2에서 선택한 Codex, Kiro CLI, Claude Code의 대화 입력창에 붙여넣습니다. Markdown 카드 다운로드
선택한 AI CLI의 대화 입력창에 붙여넣으세요. 세 도구가 같은 프롬프트를 사용합니다.
Robot Atlas 18장 정리를 수행하세요.
ISAAC_REPO/workshop/chapters/18-gpu-cleanup.md와 실제 운영 CLI를 먼저 읽으세요.
현재 isaac.py에 없는 cleanup/capacity 명령을 있다고 가정하지 마세요.
범위:
정확히 gpu-cluster-01과 RobotAtlasIsaac, RobotAtlasIsaacAddons,
RobotAtlasIsaacBuild만 정리합니다.
기존 GPU01·Jeju·운영 Fargate·공유 VPC/Subnet/NAT/endpoint·인증서·DNS는 유지합니다.
일반 참가자의 robot-lab app에서 수행하지 마세요.
순서:
1. 신규 operator 제출과 웹의 Isaac 접수를 중지한 상태를 확인합니다.
현재·실행 중 Fargate task definition은 읽기만 하며 서비스를 중지하지 않습니다.
2. active GPU 작업이 없고 소유 빌드가 종료됐는지 확인합니다.
3. 정확한 stack ID·template hash·자원 목록·Retain 데이터를 inventory에 보관합니다.
4. warm --replicas 0으로 소유 holder를 중지한 뒤 전용 컨트롤러와 완료된 소유 Job을 정리합니다.
5. isaac-sim-spot, 선택한 isaac-sim-ondemand의 NodeClass·소유권·이웃 workload를 검증하고 GPU 노드를 제거합니다.
6. 전용 build·addons를 삭제하고 CPU pool/노드를 제거한 뒤 cluster 스택을 마지막에 삭제합니다.
7. 실제 종료와 보존 데이터·이미지·로그·볼륨·잔여 비용을 기록합니다.
가드:
소유 태그, 고정 계정/VPC, inventory stack ID·template·자원 목록 불일치는 거부합니다.
알 수 없는 Job/pool/workload를 함께 지우지 마세요.
PDB/finalizer 삭제, 강제 전체 drain, EC2 전체 terminate, 전역 Docker prune을 사용하지 마세요.
S3·DynamoDB·ECR의 Retain을 유지하며 데이터 purge를 임의로 수행하지 마세요.
새 정책 제거와 기존 Fargate 역할/서비스 삭제를 혼동하지 마세요.
정리 중 workloads, warm --replicas 1, enable-ondemand 또는 새 submit을 실행하지 마세요.
DNS·계정 전체 visibility·legacy discovery 태그를 변경하지 마세요.
관리형 EC2가 기본 목록에서 안 보일 수 있으므로 저장한 NodeClaim/providerID의
특정 instance ID로 종료를 확인하세요. 빈 목록을 비용 0의 증거로 삼지 마세요.
삭제한 정확한 자원·최종 상태·보존 항목·미완료 정리와 다음 확인 시각을 보고하세요.
읽기 기록
읽음 표시는 이 브라우저에만 저장됩니다.
실제 실습 결과는 터미널에서 확인하세요.