04참가자 기본/예상 약 35분

Bedrock과 로봇 시연 연결

실제 모델의 한국어 계획으로 새 배치를 실행하고 결과와 응답 시간을 확인합니다.

5 / 5 참가자 기본아직 읽지 않음

이 장의 예상 시간은 약 35분입니다. 계정·모델 확인과 서버 전환, 실제 계획·실행, 화면 시연·결과 기록을 진행하며 준비 상태, 호출·실행 대기와 질문에 맞춰 시간을 조정합니다. 로컬 Node API가 Bedrock Converse를 직접 호출하고, MuJoCo와 저장소는 로컬에 둡니다. AgentCore Runtime 배포는 06장의 별도 실습입니다.

1. 실제 모델을 호출할 준비 확인

실제 모델을 사용할 참가자는 진행자가 확인한 AWS 역할 또는 프로필, 서울 리전에서 사용할 모델·추론 프로필, 모델 호출 권한이 필요합니다. 모델 호출은 AWS 사용량에 포함됩니다. 권한이 준비되지 않았다면 아래 호출을 반복하지 말고 이 장의 AWS 확인을 미완료로 기록합니다. 02–03장의 로컬 결과는 그대로 보존할 수 있습니다.

새 EC2 터미널에서 01장의 환경 변수를 불러온 뒤 실행합니다.

터미널bash
VSCode Server 터미널에서 실행
aws --version
python3 "$ROBOT_REPO/workshop/scripts/lab.py" doctor \
  --config "$ROBOT_CONFIG" --aws

export ROBOT_MODEL_ID="$(python3 -c 'import json,os; print(json.load(open(os.environ["ROBOT_CONFIG"]))["modelId"])')"
export ROBOT_PROFILE="$(python3 -c 'import json,os; print(json.load(open(os.environ["ROBOT_CONFIG"]))["profile"])')"
printf 'Bedrock 모델: %s\n리전: ap-northeast-2\n' "$ROBOT_MODEL_ID"

if [ -n "$ROBOT_PROFILE" ]; then
  export AWS_PROFILE="$ROBOT_PROFILE"
else
  unset AWS_PROFILE AWS_DEFAULT_PROFILE
fi

AWS CLI가 실행되고 doctor --aws가 성공해 awsChecked: true인지 확인합니다. 이 검사는 설정의 계정과 실제 AWS 호출 계정의 일치를 확인하지만, Bedrock 모델 호출이나 구조화 출력 지원까지 검사하지는 않습니다. 같은 모델의 실제 계획 응답은 아래에서 확인합니다. 참가자 API 실행기도 모델을 활성화할 때 AWS CLI로 계정을 다시 확인합니다.

프로젝트의 기본 예시는 global.openai.gpt-5.6-sol입니다. 이를 모든 계정에서 사용할 수 있다고 가정하지 않습니다. 진행자는 같은 계정·리전·권한으로 Converse의 outputConfig.textFormat을 사용하는 이 프로젝트의 호출을 미리 확인해야 합니다. 모델 목록에 보이거나 단순 채팅에 답한다는 사실만으로 이 형식을 지원한다고 판단하지 않습니다. 다른 모델을 배정받았다면 진행자가 검토한 참가자 설정의 modelId를 사용합니다.

2. API만 Bedrock 직접 호출로 전환

02장 터미널 A의 MuJoCo는 계속 실행해 둡니다. API 터미널 B에서 진행 중인 작업이 끝난 것을 확인한 뒤 Ctrl+C로 API를 종료합니다. 그 터미널에서도 01장의 환경 변수와 위의 모델·프로필 설정을 불러옵니다. 이 장의 터미널 자동 검증에서는 API와 검사 터미널 모두 ROBOT_BROWSER_ORIGIN="$ROBOT_API_URL"로 맞춥니다. HTTPS 미리보기 주소가 필요한 브라우저 실습은 자동 검증 후 전환합니다. HTTPS Origin으로 발급한 Secure 쿠키는 아래의 루프백 HTTP 요청에 전송되지 않기 때문입니다.

터미널bash
VSCode Server 터미널에서 실행
cd "$ROBOT_LAB"
export ROBOT_BROWSER_ORIGIN="$ROBOT_API_URL"
NODE_ENV=development HOST=127.0.0.1 PORT="$ROBOT_API_PORT" \
  PUBLIC_ORIGIN="$ROBOT_BROWSER_ORIGIN" \
  SIMULATION_URL="http://127.0.0.1:$ROBOT_SIM_PORT" \
  LOCAL_RUNS_DIR=.local/runs MAX_ACTIVE_RUNS=1 \
  AWS_REGION=ap-northeast-2 MODEL_ID="$ROBOT_MODEL_ID" \
  AGENTCORE_RUNTIME_ARN="" RUNS_TABLE="" REPLAY_BUCKET="" \
  ORIGIN_GATE="" SESSION_SECRET="" \
  npm run server

기동 로그의 planner는 bedrock, storage는 local이어야 합니다. AGENTCORE_RUNTIME_ARN이 있으면 AgentCore가 우선되므로 이 과정에서는 비워 둡니다. 브라우저의 Amazon Bedrock · AI 작업 계획 표시는 아래 화면 실습에서 확인합니다. 재시작 전의 실행하지 않은 계획은 메모리에서 사라지므로 새 계획을 만듭니다.

별도 터미널에서 설정 상태만 간단히 확인할 수도 있습니다.

터미널bash
VSCode Server 터미널에서 실행
.venv/bin/python - <<'PY'
import json
import os
import urllib.request
with urllib.request.urlopen(os.environ["ROBOT_API_URL"] + "/api/config", timeout=10) as response:
    config = json.load(response)
assert config["planner"] == "bedrock", config["planner"]
assert config["storage"] == "local", config["storage"]
assert config["simulation"] == "ready", config["simulation"]
print({key: config.get(key) for key in ("planner", "model", "storage", "simulation")})
PY

이 출력은 서버의 설정 증거입니다. 실제 모델 호출 성공은 다음의 새 계획 응답으로 확인합니다. 세션 쿠키와 CSRF 값은 화면이나 제출 파일에 출력하지 않습니다.

3. 실제 모델 → 새 물리 실행 → 기록 확인

다음 명령은 모델에 두 번 계획을 요청하고, 로컬 MuJoCo 실행을 두 번 생성합니다. 터미널의 별도 세션으로 수행하므로 브라우저 세션의 실행 목록에 나타나지 않을 수 있습니다. 브라우저에서 동시에 새 작업을 시작하지 않습니다.

코드는 먼저 /api/config로 세션을 만들고, 같은 쿠키와 CSRF로 계획·실행을 요청합니다. 단순 curl POST에서 이 과정을 생략하면 401 또는 403이 발생합니다.

터미널bash
VSCode Server 터미널에서 실행
cd "$ROBOT_LAB"
export ROBOT_BROWSER_ORIGIN="$ROBOT_API_URL"
.venv/bin/python - <<'PY'
import http.cookiejar
import json
import math
import os
from pathlib import Path
import time
import urllib.error
import urllib.request

base = os.environ["ROBOT_API_URL"].rstrip("/")
origin = os.environ["ROBOT_BROWSER_ORIGIN"]
assert origin == base, "터미널 검증 시 API와 검사 양쪽의 Origin을 루프백 API 주소로 맞추세요."
output = Path(".local/evidence")
output.mkdir(parents=True, exist_ok=True)
opener = urllib.request.build_opener(
    urllib.request.HTTPCookieProcessor(http.cookiejar.CookieJar())
)
csrf = None

def call(path, payload=None):
    headers = {"Accept": "application/json"}
    data = None
    if payload is not None:
        headers.update({"Content-Type": "application/json",
                        "Origin": origin, "X-CSRF-Token": csrf})
        data = json.dumps(payload, ensure_ascii=False).encode()
    request = urllib.request.Request(base + path, data=data, headers=headers)
    try:
        with opener.open(request, timeout=130) as response:
            if response.headers.get_content_type() != "application/json":
                raise RuntimeError(f"{path}: JSON 응답이 아닙니다. 포트 전달 경로를 확인하세요.")
            return json.load(response)
    except urllib.error.HTTPError as error:
        try:
            detail = json.loads(error.read()).get("error", {})
        except (ValueError, UnicodeError):
            detail = {"code": "non_json_response"}
        raise RuntimeError(f"{path}: HTTP {error.code}, {detail.get('code', 'unknown')}") from None

def save(name, value):
    (output / name).write_text(json.dumps(value, ensure_ascii=False, indent=2) + "\n")

config = call("/api/config")
assert config["planner"] == "bedrock", config["planner"]
assert config["storage"] == "local"
assert config["simulation"] == "ready"
csrf = config["csrf"]
cases = [
    ("red", "transfer", "빨간 상자만 오른쪽 트레이로 옮겨줘", 445,
     [{"objectId": "red", "target": "tray"}]),
    ("blue-first", "stack", "파란 상자, 빨간 상자, 주황 상자 순서로 쌓아줘", 2026,
     [{"objectId": name, "target": "tower"} for name in ("blue", "red", "orange")]),
]
report = []
for name, scenario, prompt, seed, actions in cases:
    started = time.perf_counter()
    plan = call("/api/plan", {"scenario": scenario, "prompt": prompt})
    plan_seconds = time.perf_counter() - started
    save(f"bedrock-{name}-plan.json", plan)
    assert plan["provider"] == "bedrock", plan["provider"]
    assert plan["model"] == config["model"]
    assert plan["scenario"] == scenario and plan["actions"] == actions, plan

    started = time.perf_counter()
    run = call("/api/runs", {"planId": plan["id"], "seed": seed})
    deadline = time.monotonic() + 240
    while run["status"] in ("queued", "running"):
        if time.monotonic() >= deadline:
            save(f"bedrock-{name}-run.json", run)
            raise TimeoutError(f"{name}: 마지막 실행 상태를 저장했습니다.")
        time.sleep(1)
        run = call("/api/runs/" + run["id"])
    run_seconds = time.perf_counter() - started
    save(f"bedrock-{name}-run.json", run)
    replay = call("/api/runs/" + run["id"] + "/replay")
    save(f"bedrock-{name}-replay.json", replay)
    assert run["status"] == replay["status"] == "succeeded", (run["status"], replay["checks"])
    assert replay["seed"] == seed and replay["scenario"] == scenario
    assert replay["engine"] == "MuJoCo"
    assert replay["provenance"]["learning"] == "none"
    assert replay["metrics"]["success"] is True
    assert replay["metrics"]["placed"] == replay["metrics"]["total"] == len(actions)
    assert replay["checks"] and all(check["passed"] for check in replay["checks"])
    assert [step["objectId"] for step in replay["steps"]] == [a["objectId"] for a in actions]
    final = replay["frames"][-1]["bodies"]
    if name == "red":
        initial = replay["frames"][0]["bodies"]
        assert final["red"]["position"][1] < -0.10
        for other in ("blue", "orange"):
            assert final[other]["position"][1] > 0.17
            assert math.dist(initial[other]["position"], final[other]["position"]) < 0.008
    else:
        for box, height in (("blue", 0.025), ("red", 0.075), ("orange", 0.125)):
            x, y, z = final[box]["position"]
            assert abs(x - 0.50) < 0.008 and abs(y + 0.20) < 0.008
            assert abs(z - height) < 0.003
    row = {"case": name, "seed": seed, "provider": plan["provider"], "model": plan["model"],
           "planSeconds": round(plan_seconds, 3), "runSeconds": round(run_seconds, 3),
           "simulationDuration": replay["duration"], "status": run["status"],
           "metrics": replay["metrics"]}
    report.append(row)
    save("bedrock-report.json", report)
    print(json.dumps(row, ensure_ascii=False))
print("실제 Bedrock 계획 2건과 새 MuJoCo 실행 2건 확인")
PY

각 경우의 plan, run, replay JSON과 bedrock-report.json이 참가자 폴더의 .local/evidence/에 생깁니다. 실행 실패 시 원인이 들어 있는 기록을 먼저 확인하고, 성공 문구가 나오지 않았는데 제출표에 완료로 적지 않습니다. 이미 실행이 생성된 뒤 연결이 끊겼다면 현재 상태를 확인하고 같은 실험을 다시 시작합니다.

4. 응답 시간을 해석하고 화면에서 시연

측정값 포함하는 범위
planSeconds 로컬 API 요청, Bedrock 왕복·모델 추론·검증
runSeconds 실행 생성, 물리 계산·로컬 저장, 최대 약 1초의 폴링 간격
simulationDuration 가상 시간으로 기록된 동작 길이

이 결과는 AgentCore Runtime의 지연을 측정하지 않습니다. Runtime과 직접 호출을 비교하려면 같은 모델·요청·리전·준비 상태로 반복 측정하는 별도 실험이 필요합니다. 첫 구조화 출력 요청은 스키마 준비 등의 영향을 받을 수 있으므로, 첫 요청과 반복 요청을 구분해 적습니다. 다른 환경의 1–2초 관측값을 자신의 목표 시간이나 보장값으로 사용하지 않습니다.

PC의 같은 포트로 전달된 HTTP 주소를 사용하는 참가자는 브라우저를 새로고침합니다. 별도 HTTPS 미리보기 주소가 필요한 참가자는 자동 검증이 끝난 뒤 API 터미널 B에서 Ctrl+C로 종료하고 다음 값을 설정합니다.

터미널bash
VSCode Server 터미널에서 실행
read -r -p "배정된 HTTPS 미리보기 Origin: " ROBOT_BROWSER_ORIGIN
export ROBOT_BROWSER_ORIGIN

그 뒤 이 장의 API 기동 명령에서 export ROBOT_BROWSER_ORIGIN="$ROBOT_API_URL" 줄을 제외하고 NODE_ENV=development부터 npm run server까지 다시 실행합니다. 배정 주소는 경로가 없는 HTTPS Origin이어야 합니다. 브라우저를 새로고침해 그 주소의 세션을 만들고 새 계획부터 진행합니다. 쿠키의 Secure 속성이나 CSRF 검사를 제거해 접속 방식을 섞지 않습니다.

두 지시 중 하나로 새 계획을 만듭니다. 제공자 Amazon Bedrock · AI 작업 계획, 물체 순서, 배치 번호를 확인한 뒤 실행합니다. 완료 후 일시정지·상단 시점·검증 결과·JSON 다운로드를 보여 줍니다. 이 브라우저 실습은 위 자동 확인에 더해 모델 호출과 물리 실행이 한 번 추가됩니다.

5. 오류를 구분해서 해결

증상·오류 코드 뜻과 다음 확인
planner_access_denied 역할·프로필, 배정 계정, 모델·추론 프로필 호출 권한 확인
planner_unavailable 모델 ID·리전·Converse 구조화 출력 지원 확인. 단순 텍스트 모델로 임의 교체하지 않음
planner_timeout 첫 요청인지, 모델 상태·지연·허용 시간을 확인. 요청을 연속으로 쌓지 않음
planner_throttled 모델 할당량·참가자 동시 호출 확인. 진행자와 실행 시간을 분산
planner_invalid_output 모델 응답이 허용 계약을 통과하지 못함. 필드 검증을 제거하지 않음
planner가 rules API 기동 터미널의 MODEL_ID와 실제 포트 확인
planner가 agentcore 기동 환경의 AGENTCORE_RUNTIME_ARN 확인. 이 장에서는 직접 호출 사용
session_required, csrf_rejected, origin_rejected 동일 쿠키·CSRF·Origin 확인. HTTPS용 Secure 쿠키를 HTTP에 보내고 있지 않은지 확인
not_found 계획의 15분 만료·API 재시작·다른 세션 여부 확인 후 새 계획 생성
simulation_busy 또는 429 진행 중인 실행 종료를 기다림. 자동 무제한 재시도 금지

클라우드가 설정된 상태에서 호출이 실패하면 API는 오류를 반환합니다. 규칙 기반으로 계속 실습하려면 02장의 명령으로 API를 명시적으로 다시 시작하고 결과 제공자를 rules로 기록합니다. 그것을 Bedrock 성공으로 제출하지 않습니다.

6. 결과 기록과 로컬 종료

VSCode에서 참가자 폴더의 RESULTS.md를 만들고 실제 수행한 값으로 작성합니다. 예시 숫자와 다른 사람의 성공 기록을 그대로 옮기지 않습니다.

항목 제출할 근거
작업 경계 팀 이름, 참가자 폴더, 사용한 AI CLI·버전
계획 검사 수정한 테스트 파일, 실제 실행 명령·통과/실패
물리 실험 단독 이동·파랑부터 쌓기·힘 0 실패 기록 경로
실제 모델 bedrock 여부, 모델 ID, 두 계획의 물체 순서
새 실행 배치 번호, placed/total, 모든 물리 검사, 실패가 있으면 원인
시간 계획·실행 소요 시간과 가상 기록 길이
브라우저 접속한 참가자 주소, 새 실행과 JSON 확인 여부
종료 본인 API·MuJoCo 중지 여부, 미완료 항목

교재의 읽음 표시는 이 결과를 대신하지 않습니다. AgentCore, Fargate, CloudFront, EKS·Isaac Sim 배포 여부는 핵심 과정 완료표에 넣지 않습니다.

심화를 바로 이어 가지 않는다면 API 터미널 B, MuJoCo 터미널 A 순서로 Ctrl+C를 누릅니다. 본인 프로세스의 종료 로그와 해당 포트의 연결 종료를 확인합니다. 이 과정에서는 AWS 인프라를 새로 생성하지 않았으므로 별도 스택 삭제 명령이 없습니다. VSCode Server용 EC2나 공유 VPC·NAT는 진행자의 관리 대상입니다. 참가자 증거 파일은 보관하고 쿠키·토큰·인증 파일을 제출하지 않습니다.

Codex프롬프트 입력

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

현재 참가자 폴더의 RESULTS.md와 .local/evidence/bedrock-report.json을 검토해 주세요.
실제 모델 호출, 물리 성공, 규칙 기반 실행, 미실행 항목을 나누어 근거가 맞는지 확인하세요.
planSeconds에 AgentCore 지연이나 물리 계산 시간이 포함되었다고 쓰지 마세요.
결과 파일이 없거나 검사가 실패했으면 그 상태 그대로 기록하고, 새로운 AWS 자원은 만들지 마세요.

마칠 때 확인

  • 계정 확인과 실제 모델 계획 응답을 구분했다.
  • 두 지시의 provider: bedrock와 정확한 actions를 확인했다.
  • 각 계획으로 새 MuJoCo 실행을 만들고 결과 JSON의 물리 검사를 확인했다.
  • 계획 지연·실행 지연·시뮬레이션 시간을 구분해 기록했다.
  • 미완료 항목과 본인 프로세스의 종료 여부를 남겼다.

프롬프트: 실제 연결과 결과 검토

핵심 과정 종료 · 이전: 03. 작업 계획 검증

선택 심화: 05. ECR·S3·DynamoDB 준비

AI CLI 프롬프트 카드

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

Codex프롬프트 입력

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

현재 참가자 로컬 API가 Bedrock을 직접 호출하는지 확인하는 실습입니다.
공개 운영 주소가 아니라 교재 info에서 확인한 내 API 포트를 사용합니다.
server/config.ts, server/planner.ts, server/app.ts와 교재 04장의 HTTP 절차를 확인하세요.

먼저 다음 구성을 확인하세요. 인증 파일이나 비밀값은 출력하지 마세요.
- MODEL_ID는 진행자가 해당 계정에서 검증한 모델·추론 프로필.
- AWS_REGION은 ap-northeast-2.
- AGENTCORE_RUNTIME_ARN은 비어 있어야 함.
- RUNS_TABLE과 REPLAY_BUCKET도 비어 있어 로컬 저장소를 사용.
- 터미널 자동 검증에서는 PUBLIC_ORIGIN과 요청 Origin을 내 루프백 API 주소로 통일.
- HTTPS 미리보기는 자동 검증 후 API를 재시작해 배정 Origin으로 전환.

GET /api/config의 planner=bedrock은 설정 증거이고 실제 호출 성공 증거는 아닙니다.
새 /api/plan 응답에서 provider와 model, actions를 확인해야 합니다.
POST에는 같은 세션 쿠키, X-CSRF-Token, Origin, Content-Type을 사용하세요.
헤더의 토큰이나 쿠키를 답변·로그·RESULTS.md에 넣지 마세요.
HTTPS용 Secure 쿠키를 HTTP로 보내기 위해 속성이나 검증을 제거하지 마세요.

확인할 지시는 빨간 상자만 트레이로 옮기기와 파랑→빨강→주황 쌓기입니다.
계획의 actions가 맞는지 먼저 검증하고 planId와 seed로 새 실행을 만듭니다.
실행 JSON의 seed, checks, metrics, 실제 물체 위치를 읽습니다.
참가자가 이미 교재 명령을 실행했다면 새 호출을 반복하지 말고 기존 근거부터 읽으세요.

오류가 나면 HTTP 상태와 안전한 error.code로 계정·모델·구조화 출력·세션·물리 문제를 나누세요.
규칙 기반 계획이나 공개 운영 서비스 결과로 대체하지 마세요.
이 요청은 Runtime·Fargate·CloudFront·EKS 배포와 DNS 변경을 포함하지 않습니다.

최종 검토에서는 다음 기준을 사용하세요.
현재 참가자 폴더의 RESULTS.md와 .local/evidence/에 있는 실제 결과만 검토해 주세요.
규칙 기반 실행, 실제 Bedrock 계획, 새 MuJoCo 물리 실행, 실패 실험, 미완료 항목을 구분하세요.
수정 파일과 실행 명령, 계획의 물체 순서, 배치 번호, 물리 검사, 소요 시간을 남기세요.
planSeconds는 API·Bedrock 왕복과 추론을 포함하며 AgentCore 지연 측정값이 아닙니다.
runSeconds와 simulationDuration도 구분해 주세요.
AWS 배포, Isaac Sim, Spot 복구를 검증하지 않았다면 완료 목록에 넣지 마세요.
로컬 서버의 종료 여부와 남긴 참가자 결과 파일을 기록하세요.

읽기 기록

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