DEV Community

Kyungmin (Lucas) Kim for JANUS

Posted on with Donghee (Chad) Kim • Edited on

Bedrock AgentCore harness 로 에이전트 PoC 후기

요즘 에이전트 PoC를 한 번이라도 해본 분이라면 비슷한 경험이 있을 겁니다. 모델 호출 루프 자체는 하루면 만드는데, 막상 "여러 사용자에게 서비스하려면?"이라는 질문이 들어오는 순간부터 일이 폭발합니다. 격리된 샌드박스, 세션별 메모리, 도구 인증, 관측성, 시크릿 관리, 컨테이너 빌드까지. 정작 모델의 지능은 잘 작동하는데 그 주변 배선 때문에 프로덕션까지 못 가는 경우가 많았습니다.

그러던 차에 Amazon Bedrock AgentCore harness is now generally available라는 글을 읽었습니다. "에이전트가 프로덕션에서 돌아가기 위해 필요한 모든 배선을 두 개의 API 호출 뒤로 숨겨주는 managed abstraction"이라는 설명이 흥미로워서, 바로 AWS 계정에서 직접 만들어 테스트해봤습니다.

간단히 말하면 AgentCore harness는 에이전트 오케스트레이션 루프를 "코드"가 아니라 "설정"으로 다루게 해주는 관리형 레이어입니다. 모델, 도구, 스킬, 메모리, 시스템 프롬프트를 선언적으로 넘기면 AgentCore가 격리된 microVM, 메모리 스토어, ID 토큰 볼트, 관측성 파이프라인까지 묶어서 실행해줍니다. 내부적으로는 AWS의 오픈소스 에이전트 프레임워크인 Strands Agents가 루프를 돌리는 구조입니다.

여기서, 그냥 Bedrock Agents 쓰면 안되나? 라는 생각이 들 수 있어 정리한 비교표입니다.

Bedrock Agents LangGraph Strands Agents (AgentCore Harness)
아키텍처 AWS 완전 관리형 (블랙박스) 그래프 기반 상태 머신 (self-hosted) 심플 루프 + 도구 위임 (managed or self-hosted)
모델 Bedrock 전용 아무거나 Bedrock + OpenAI + Gemini + LiteLLM
도구 연결 Lambda Action Group 필수 직접 코딩 MCP 생태계 네이티브
런타임 블랙박스 직접 운영 (EC2/ECS) AgentCore microVM 또는 직접 운영
코드 실행 X (Lambda 별도) 직접 구현 O Code Interpreter 내장
멀티 모델 X 에이전트당 1개 O 노드별 가능 O 턴별 스위칭
메모리 세션 내 한정 직접 구현 Managed Memory (세션 간 영속, actorId 격리)
탈출구 X vendor lock-in 처음부터 코드 O export → Python 코드
배포 복잡도 낮음 (콘솔 클릭) 높음 (인프라 직접) 중간 (API 2개로 끝)
적합한 케이스 단순 챗봇, RAG Q&A 복잡한 멀티스텝 워크플로우 도구 사용 많은 작업 실행형 에이전트

Strands Agents + AgentCore Harness는 중간을 노렸습니다. Strands의 심플한 루프(시스템 프롬프트 → 모델 판단 → 도구 호출 → 반복)는 LangGraph처럼 그래프를 설계할 필요 없이 모델이 알아서 판단하는 구조이고, AgentCore가 그 위에 microVM, 메모리, 관측성을 씌워주니 인프라 코드 없이 프로덕션에 올릴 수 있습니다. 거기다 나중에 export 한 줄로 Strands Python 코드로 변환되니까 lock-in 걱정도 없습니다.

이제 실제로 써봐야겠죠?
PoC 목표는 "AWS 운영 데이터를 조사해서 분석 결과를 PPT로 만들어주는 에이전트"를 처음부터 끝까지 만들어보는 것이었습니다. 모델 갈아끼우기, 메모리 유지, S3에 결과물 떨어뜨리기까지 전부 한 harness로 끝낼 수 있는지 확인하는 게 핵심이었습니다.

(A) 설계 의도와 아키텍처

AgentCore harness의 핵심 아이디어는 한 줄로 요약됩니다. 에이전트가 하는 일은 선언하고, 그게 돌아가기 위한 인프라는 AgentCore가 책임진다. CreateHarness로 정의하고 InvokeHarness로 실행하는, 딱 두 개의 API 호출이 사용자 경계입니다.

그 뒤에 Runtime(microVM 호스팅), Memory(대화 영속화), Gateway(도구 연결), Browser/Code Interpreter(샌드박스 도구), Identity(자격 증명 볼트), Observability(추적)가 자동으로 묶여 들어갑니다.

저장과 실행 모델이 흥미로운 지점입니다. 모든 세션은 Firecracker microVM 위에서 격리되어 돌아가고, 각 세션은 고유의 파일시스템과 shell을 갖습니다. 같은 runtimeSessionId로 다시 호출하면 microVM이 재기동되더라도 메모리에 저장된 대화가 자동으로 복원됩니다. 즉, 메시지 히스토리를 직접 들고 다닐 필요가 없습니다. actorId(사용자를 식별하는 메모리 네임스페이스 키)를 같이 넘기면 사용자별로 메모리 공간이 격리됩니다. 이게 멀티 테넌시 처리의 출발점입니다.

또 하나 인상적이었던 건 모델 스위칭입니다. harness는 Bedrock(Claude, Nova, Llama, OpenAI on Bedrock 등), OpenAI 직결, Gemini, LiteLLM 호환 프로바이더를 모두 지원하는데, 같은 세션 안에서 턴 단위로 프로바이더를 바꿔도 컨텍스트가 유지됩니다. "계획은 Opus로, 코드는 GPT로, 요약은 Gemini로" 같은 패턴이 설정만으로 가능합니다. 외부 프로바이더의 API 키는 AgentCore Identity의 토큰 볼트에 저장되고, 에이전트 코드는 raw credential을 보지 않습니다.

저는 이걸 AWS에 어떻게 얹었는지부터 정리해보겠습니다. PoC라 새로 만든 IAM 실행 역할 하나(MyHarnessRole)에 Bedrock 모델 호출(AmazonBedrockFullAccess), CloudWatch Logs, X-Ray, AgentCore 전체(BedrockAgentCoreFullAccess) 권한을 붙였습니다.
managed policy 4개로 끝났습니다.
다만 참고사항으로, 지정 S3 버킷에 결과물을 올리려면 실행 역할에 해당 버킷의 PutObject 권한을 inline으로 추가해야 합니다. managed policy 4개만으로는 AgentCore runtime 버킷에만 쓸 수 있습니다!

컴퓨트는 AgentCore Runtime이 알아서 처리하므로 EC2를 따로 띄우지 않았고, 호출자(저)는 로컬에서 boto3로 SigV4 호출만 했습니다. 결과물 업로드용으로는 지정 S3 버킷에 PutObject 권한을 inline으로 추가했습니다.

다음은 가장 단순한 형태의 harness 생성 코드입니다. 모델은 기본값(Claude Sonnet)을 쓰고, 도구로 Code Interpreter와 Browser만 붙였습니다.

# harness 생성: 설정만으로 에이전트 정의
import boto3
control = boto3.client("bedrock-agentcore-control", region_name="us-west-2")

resp = control.create_harness(
    harnessName="aws_analytics_agent",
    executionRoleArn="arn:aws:iam::123456789012:role/MyHarnessRole",
    systemPrompt=[{"text": "AWS 운영 데이터 분석 어시스턴트입니다."}],
    tools=[
        {"type": "agentcore_code_interpreter", "name": "code_interpreter"},
        {"type": "agentcore_browser", "name": "browser"},
    ],
    # skills 미지정 시 비활성, memory 미지정 시 managed memory 자동 생성
    skills=[{"awsSkills": {"paths": ["core-skills/*", 
                                     "specialized-skills/operations-skills/*"]}}],
)
print(resp)  # 전체 응답 확인 -> harnessArn, status 키 확인
Enter fullscreen mode Exit fullscreen mode

다음은 harness 호출 코드입니다.

import boto3

runtime = boto3.client('bedrock-agentcore', region_name='us-west-2')

response = runtime.invoke_harness(
    harnessArn='arn:aws:bedrock-agentcore:us-west-2:123456789012:harness/aws_analytics_agent-qok4nFzuv0',
    runtimeSessionId='session-00000000-0000-0000-0000-000000000001',
    actorId='user-username',
    messages=[{
        "role": "user",
        "content": [{"text": "현재 계정의 EC2, S3, Lambda 사용 현황을 분석하고 PPT로 만들어서 s3://test-bucket/output/ 에 업로드해줘"}]
    }]
)

for event in response['stream']:
    if 'contentBlockDelta' in event:
        text = event['contentBlockDelta']['delta'].get('text', '')
        print(text, end='', flush=True)
    elif 'metadata' in event:
        usage = event['metadata']['usage']
        print(f"\n\n---\n입력: {usage['inputTokens']} / 출력: {usage['outputTokens']} 토큰")
Enter fullscreen mode Exit fullscreen mode

▲ InvokeHarness 한 번으로 리소스 조회 → PPT 생성 → S3 업로드까지 완료

▲ 지정 버킷에 PPT가 업로드된 모습

awsSkills는 AWS가 큐레이션한 스킬 번들(SDK 사용법, IAM, CloudWatch, EC2 등)을 토글 한 번으로 켜는 기능입니다. 별도 URL이나 네트워크 fetch 없이 harness 내부 런타임에 미리 들어있어서, 에이전트가 "EC2 인스턴스 상태 조회"라는 작업을 만나면 그때 해당 스킬만 컨텍스트로 끌어옵니다. 이 progressive disclosure 방식 덕분에 컨텍스트 윈도우가 불필요한 지침으로 채워지지 않습니다.

(B) Deep Dive: 주요 이점 및 고려 사항

테스트하면서 가장 체감했던 건 "바꾸기"가 정말 설정 변경으로 끝난다는 점이었습니다. 모델을 Sonnet에서 Opus로 갈아끼우는 작업, 도구를 하나 추가하는 작업, 시스템 프롬프트를 다듬는 작업 모두 컨테이너 재빌드 없이 update-harness 한 번으로 처리됩니다. PoC에서 모델 비교를 위해 5번 정도 모델을 바꿨는데, 각 변경이 분 단위가 아니라 초 단위였습니다. 컨테이너 빌드/푸시 단계가 사라진 게 가장 컸습니다.

메모리 동작도 인상적이었습니다.

▲ 같은 Session ID로 후속 질문 — 이전 분석 결과를 정확히 기억하고 요약

CreateHarness에 memory를 명시하지 않으면 SEMANTIC + SUMMARIZATION 전략과 30일 이벤트 만료를 가진 managed memory가 자동으로 프로비저닝됩니다. 중요한 건 이게 추상화된 블랙박스가 아니라 계정 안에 실제로 존재하는 AgentCore Memory 리소스라는 점입니다. ARN으로 조회 가능하고, 다른 harness에 붙일 수도 있고, 분석 파이프라인에 그대로 흘려보낼 수도 있습니다. PoC에서 actorId="user-poc-01"로 첫 대화를 한 뒤 새 세션을 열어 "내가 어제 뭐 물어봤었지?"라고 묻자, 직전 세션에서 추출된 요약과 사실들이 context에 자동 주입되면서 정확히 응답했습니다.

AWS skills를 켜면 에이전트의 LLM 판단 턴 수가 눈에 띄게 줄었습니다.
스킬에 "Athena 쿼리할 때는 이 절차로", "S3 객체 나열은 이 페이지네이션 패턴으로"
같은 절차적 지식이 미리 들어있어서, 모델이 시행착오 없이 한 번에 올바른 경로를 잡습니다.
tool call 자체는 작업 복잡도에 따라 수십 회까지 나오지만,
불필요한 재시도가 사라진 게 핵심입니다. 토큰 비용도 같이 떨어졌습니다.

운영 측면에서 짚어둘 고려 사항도 있습니다. 첫째, 콜드 스타트입니다. microVM이 새로 뜨는 첫 invocation은 1~3초 정도의 추가 지연이 있었습니다. idleRuntimeSessionTimeout(기본 900초)을 늘리면 warm 상태를 더 오래 유지할 수 있지만, 그만큼 active-consumption 비용이 늘어납니다. PoC에서는 600초로 줄여서 비용을 우선했습니다. 둘째, VPC 모드의 NAT 비용입니다. EFS나 S3 Files 마운트를 쓰려면 VPC가 필수이고, harness는 매 세션 시작 시 ECR Public에서 컨테이너 이미지를 pull하기 때문에 NAT 게이트웨이가 필요합니다. ECR Public은 VPC endpoint를 지원하지 않습니다. 데이터 전송량이 많지 않은 PoC에서는 무시할 수준이었지만, 프로덕션에서는 미리 계산해두는 게 좋습니다.

셋째, shared responsibility가 생각보다 넓습니다. InvokeHarness로 들어오는 입력은 인증을 통과한 모든 호출자에게 신뢰되는 입력으로 취급됩니다. 특히 model.additionalParams 같은 필드는 그대로 프로바이더에 전달되기 때문에, LiteLLM의 aws_bedrock_runtime_endpoint나 OpenAI의 extra_headers 같은 파라미터를 통해 요청 경로 자체가 바뀔 수 있습니다. 외부에 노출하는 경우 애플리케이션 레이어에서 model 필드를 allowlist로 거르는 게 안전합니다. 문서에 명시되어 있는 부분인데, PoC 단계라도 한 번 점검해둘 만한 포인트입니다.

비용 모델은 active-consumption 기반이라 의외로 합리적이었습니다. Runtime은 vCPU-hour $0.0895, GB-hour $0.00945로 과금되는데, 에이전트가 모델 응답을 기다리는 동안에는 CPU를 거의 안 쓰니까 청구 시간도 짧습니다. PoC 일주일 동안 약 200회 invocation을 돌려서 Runtime 비용은 $3 정도, 모델 추론 비용이 그보다 훨씬 컸습니다. harness 자체에는 별도 요금이 없고, 사용한 primitive 단위로만 청구되는 구조라 비용 추적이 깔끔했습니다.

(C) 아키텍처 트레이드오프

가장 먼저 고민한 건 harness vs Runtime 직접 사용이었습니다. AgentCore Runtime은 내가 코드를 짜고 컨테이너에 담아서 올리는 서버리스 호스팅이고, harness는 그 위에 Strands 기반 루프를 얹어준 관리형 추상화입니다. 자유도는 Runtime이 높고(루프, 훅, 그래프 형태의 워크플로우 가능), 시작 속도와 운영 부담은 harness가 압도적으로 낮습니다. PoC 목적이 "최소 시간에 프로덕션 형태 확인"이었기 때문에 harness를 선택했습니다. 만약 멀티 에이전트 그래프나 양방향 스트리밍, 커스텀 훅이 필요했다면 Runtime이 답이었을 겁니다.

다음은 메모리 옵션입니다. managed memory(자동 프로비저닝)와 BYO memory(직접 만든 Memory 리소스 ARN 연결) 중에서, PoC에서는 managed memory를 선택했습니다. 이유는 두 가지인데, 첫째로 30일 만료와 SEMANTIC+SUMMARIZATION 전략 기본값이 PoC에는 충분했고, 둘째로 나중에 UpdateHarness로 BYO로 갈아탈 수 있는 경로가 명시적으로 열려 있었기 때문입니다. 포기한 건 KMS 고객 키로 암호화하거나 namespace 템플릿을 직접 짜는 자유도였는데, 프로덕션 전환 시점에 BYO로 옮기면 됩니다.

세 번째는 파일 출력 전략입니다. harness는 session storage(세션 내 임시),
EFS access point(공유), S3 Files access point(양방향 동기화) 세 가지
파일시스템 마운트를 지원합니다.

PoC에서는 별도 마운트 없이, 에이전트가 shell 환경에서 boto3 upload_file()
직접 S3에 업로드하는 방식을 썼습니다. Code Interpreter(샌드박스)에는
AWS 자격증명이 주입되지 않기 때문에, PPT 생성은 Code Interpreter에서,
업로드는 shell 도구로 전환해서 처리합니다. 실행 역할에 해당 버킷
PutObject 권한만 추가하면 동작했고, VPC나 NAT 없이도 가능했습니다.

프로덕션에서 여러 harness가 같은 파일을 공유하거나, 양방향 동기화가
필요하다면 S3 Files access point를 검토하면 됩니다 (VPC 필수).
프로덕션에서 최소 권한으로 좁힐 때의 참고 예시입니다.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AgentCoreMemory",
      "Effect": "Allow",
      "Action": [
        "bedrock-agentcore:CreateEvent",
        "bedrock-agentcore:RetrieveMemoryRecords"
      ],
      "Resource": "arn:aws:bedrock-agentcore:us-west-2:123456789012:memory/*"
    },
    {
      "Sid": "BedrockModelInvocation",
      "Effect": "Allow",
      "Action": ["bedrock:InvokeModel", "bedrock:InvokeModelWithResponseStream"],
      "Resource": "arn:aws:bedrock:us-west-2::foundation-model/*"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

마지막은 versioning과 endpoint 전략입니다. harness는 UpdateHarness를 호출할 때마다 immutable한 버전이 생성되고, DEFAULT endpoint는 자동으로 최신을 따라갑니다. 명시적으로 PROD, STAGING 같은 named endpoint를 만들면 해당 endpoint는 명시적 promotion 전까지 고정됩니다. PoC에서는 DEFAULT만 썼지만, 프로덕션에서는 PROD를 V2에 고정하고 새 변경은 DEFAULT(V3)에서 검증한 뒤 PROD를 V3로 점프시키는 방식이 자연스러워 보였습니다. 롤백도 endpoint를 이전 버전으로 가리키기만 하면 끝납니다.

다음은 PoC에서 사용한 invoke 호출입니다. 세션 ID를 33자 이상으로 맞춰야 한다는 점만 주의하면 됩니다.

# invoke: 같은 sessionId로 다시 부르면 대화가 이어집니다
import boto3, uuid
data = boto3.client("bedrock-agentcore", region_name="us-west-2")

session_id = str(uuid.uuid4()).ljust(33, "0")  # 33자 이상 필수
resp = data.invoke_harness(
    harnessArn="arn:aws:bedrock-agentcore:us-west-2:123456789012:harness/aws_analytics_agent_xxx",
    runtimeSessionId=session_id,
    actorId="user-poc-01",  # 사용자별 메모리 격리
    messages=[{"role": "user", 
               "content": [{"text": "지난 7일간 us-west-2의 EC2 CPU 사용률 상위 5개 인스턴스를 분석해줘."}]}],
)

for event in resp["stream"]:
    if "contentBlockDelta" in event:
        delta = event["contentBlockDelta"].get("delta", {})
        if "text" in delta:
            print(delta["text"], end="", flush=True)
Enter fullscreen mode Exit fullscreen mode

세션 중간에 모델을 갈아끼우는 것도 시험해봤습니다. 같은 sessionId로 첫 턴은 Sonnet, 두 번째 턴은 Opus를 명시했더니 컨텍스트 손실 없이 매끄럽게 이어졌습니다.

# 같은 세션에서 모델만 바꿔 호출 - 컨텍스트 유지됨
resp = data.invoke_harness(
    harnessArn=HARNESS_ARN,
    runtimeSessionId=session_id,
    model={"bedrockModelConfig": 
           {"modelId": "us.anthropic.claude-opus-4-5-20251101-v1:0"}},
    messages=[{"role": "user", 
               "content": [{"text": "방금 분석한 결과를 PPT 슬라이드 3장으로 정리해줘."}]}],
)
Enter fullscreen mode Exit fullscreen mode

관측성은 따로 설정한 게 없는데도 CloudWatch GenAI Observability의 Harnesses 탭에서 세션별 trace가 깔끔하게 보였습니다. 모델 호출, 도구 호출, 메모리 작업이 한 화면에서 시간 순으로 정렬되고, 각 span의 input/output이 inline으로 펼쳐졌습니다. 기존에 로그 그룹 다섯 개를 열어보던 패턴이 한 화면으로 줄었습니다.

Summary & Final Checklist

PoC에서 내린 주요 결정은 이렇게 정리됩니다.
컴퓨트는 harness가 관리하는 microVM 풀(EC2 직접 운영 없음),
메모리는 managed memory로 시작해 향후 BYO 전환 경로 확보,
파일 출력은 shell에서 boto3로 S3 직접 업로드 (VPC 불필요),
IAM은 managed policy 4개로 빠르게 시작하되 프로덕션 전환 시 최소 권한으로 좁힐 예정,
그리고 endpoint는 PoC에서는 DEFAULT만 사용했습니다.

복제하려는 분을 위한 체크리스트입니다.

  • 실행 역할에 managed policy 4개 부여: AmazonBedrockFullAccess, CloudWatchLogsFullAccess, AWSXRayWriteOnlyAccess, BedrockAgentCoreFullAccess. 지정 S3 버킷 사용 시 PutObject 권한 inline 추가.
  • CloudWatch Transaction Search를 계정에서 한 번 활성화 (관측성 trace 표시에 필요)
  • runtimeSessionId는 33자 이상으로 생성 (uuid + padding)
  • VPC 모드 사용 시 NAT 게이트웨이로 public.ecr.aws 도달 가능한지 확인
  • additionalParams 같은 모델 설정 필드를 외부에서 받지 않도록 애플리케이션 레이어에서 차단
  • 비용 통제용으로 maxIterations, timeoutSeconds, maxTokens를 처음부터 보수적으로 설정

Lessons Learned

가장 놀라웠던 건 "config-to-code graduation" 개념이었습니다.
agentcore export harness --arn <arn> 한 줄로 Strands 기반 Python 코드가 생성되며, 모델/도구/메모리/스킬 배선이 그대로 보존됩니다. 생성된 코드는 AgentCore Runtime에 그대로 배포하거나, Lambda/ECS/K8s 어디든 self-hosted로 옮길 수 있습니다.처음부터 코드로 시작하지 않아도 되는 안전망이 있다는 점이 가장 마음에 들었던 설계였습니다.

다음번에는 두 가지를 다르게 할 것 같습니다. 첫째, actorId와 세션 ID 관리 전략을 처음부터 정해두는 것입니다. 사용자별 메모리 격리는 actorId 하나로 깔끔하게 처리되지만, 세션 ID를 어떻게 발급하고 재사용할지에 대한 정책이 없으면 대화 연속성이 의도와 달라집니다. 둘째, 컨테이너 이미지를 처음부터 가져가지 말고 기본 환경으로 시작하는 것입니다. 처음에 "어차피 커스텀 의존성 필요하겠지" 하고 Dockerfile부터 만들었는데, 결국 기본 환경의 Python + bash로도 충분했고 ECR 푸시 단계가 사라지자 반복 속도가 두 배는 빨라졌습니다.

마지막으로, AgentCore harness는 "에이전트가 어렵다"라는 명제의 무게중심을 옮겨준 느낌이었습니다. 어려운 건 루프가 아니라 그 주변 배선이었고, 그 배선을 설정으로 다루게 되면서 "어떤 에이전트를 만들지" 자체에 시간을 쓸 수 있게 됩니다. PoC를 며칠이 아니라 몇 시간 단위로 끊어 돌릴 수 있다는 게 결과적으로 더 많은 아이디어를 시도하게 만들었습니다.

Kyungmin Kim Kyungmin (Lucas) Kim
주식회사 이테크시스템의 AWS Solutions Architect로서, 실무 중심의 서버리스 엔지니어링과 전략적 AI 전환(AX)의 가교 역할을 하고 있습니다. 단순한 시스템 구현을 넘어 지능적이고 자율적인 환경을 전략적으로 설계하는 데 집중하며, AWS 생태계의 지속적인 발전에 기여하고자, 실전에서 얻은 아키텍처 패턴과 기술적 통찰을 기술 커뮤니티와 적극적으로 공유하고 있습니다.
Donghee Kim Donghee (Chad) Kim
주식회사 이테크시스템의 AWS Solutions Architect이자 테크 에반젤리스트입니다. 기업 고객이 AWS와 AI를 비즈니스에 효과적으로 도입할 수 있도록 '비즈니스 퍼스트' 관점의 아키텍처 설계와 보안 컴플라이언스를 고려한 클라우드 보안 컨설팅을 지원하고 있으며, AWS Summit 2026에서 AX 기반 상품 전략 플랫폼 구축 사례(PRT302-S) 발표 및 AWS 13x Certified (Golden Jacket) 자격을 보유하고 있습니다.

Top comments (0)