<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Kyungmin (Lucas) Kim</title>
    <description>The latest articles on DEV Community by Kyungmin (Lucas) Kim (@kimmin1kk).</description>
    <link>https://dev.to/kimmin1kk</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3903496%2Fa506704a-1d05-4b83-907f-34baee78b0f1.png</url>
      <title>DEV Community: Kyungmin (Lucas) Kim</title>
      <link>https://dev.to/kimmin1kk</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kimmin1kk"/>
    <language>en</language>
    <item>
      <title>Claude Code OTel 메트릭을 CloudWatch Coding Agent Insights로 보내본 후기</title>
      <dc:creator>Kyungmin (Lucas) Kim</dc:creator>
      <pubDate>Mon, 03 Aug 2026 00:02:48 +0000</pubDate>
      <link>https://dev.to/janus/claude-code-otel-meteurigeul-cloudwatch-coding-agent-insightsro-bonaebon-hugi-486a</link>
      <guid>https://dev.to/janus/claude-code-otel-meteurigeul-cloudwatch-coding-agent-insightsro-bonaebon-hugi-486a</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxxhv0njqzjia8a2qmn45.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxxhv0njqzjia8a2qmn45.png" alt="Image description0" width="800" height="336"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;요즘 팀에서 Claude Code, Codex 같은 코딩 에이전트를 도입하는 조직이 늘고 있는데, 막상 "누가 얼마나 쓰고 있는지", "어느 팀이 토큰을 많이 소비하는지" 같은 질문에는 답하기 어렵습니다. 개발자 개개인의 로컬 터미널에서 돌아가는 도구라, IT 조직 관점에서는 블랙박스에 가깝습니다. 그러다가 최근 &lt;a href="https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/coding-agents-insights.html" rel="noopener noreferrer"&gt;Coding Agent Insights - Amazon CloudWatch&lt;/a&gt; 문서를 읽게 됐는데, CloudWatch가 OTLP 엔드포인트로 코딩 에이전트의 OpenTelemetry 메트릭을 받아서 전용 대시보드로 보여준다는 내용이었습니다.&lt;/p&gt;

&lt;p&gt;호기심이 생겨서 개인 AWS 계정에 Claude Code를 붙여보고, 팀 시나리오를 흉내내기 위해 EC2 위에 여러 사용자 프로파일을 만들어 테스트해봤습니다. 결론부터 말하면, 대시보드 자체는 CloudWatch 콘솔에 기본 제공되는 리소스지만 실제로 값이 채워지려면 에이전트 쪽에서 OTel 리소스 속성을 정확한 형태로 보내야 하고, 여기서 은근히 삽질 포인트가 많았습니다.&lt;/p&gt;

&lt;p&gt;이 글에서는 코딩 에이전트가 어떤 텔레메트리를 어떻게 뽑아내는지, CloudWatch의 네이티브 OTLP 엔드포인트가 그걸 어떻게 받아 처리하는지를 먼저 정리하고, 이어서 EC2 기반 개발 환경에서 어떻게 배포했는지, 그리고 무엇을 취사선택했는지를 공유합니다.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvayptc0lprq50mfsprky.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvayptc0lprq50mfsprky.png" alt="Image description1" width="800" height="328"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;범위 명시&lt;/strong&gt;: 이 글은 Coding Agent Insights의 &lt;strong&gt;metrics&lt;/strong&gt; 경로에 집중합니다. tool 실행 결과 같은 events/logs나 traces는 별도 exporter와 opt-in이 필요하며, 여기서는 다루지 않습니다(뒤의 "시그널 구분" 참고).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  (A) 설계 의도와 아키텍처
&lt;/h2&gt;

&lt;p&gt;Claude Code, OpenAI Codex, GitHub Copilot 같은 최신 코딩 에이전트는 이미 내부에 OpenTelemetry SDK를 내장하고 있습니다. 사용자가 프롬프트를 입력할 때마다 세션 단위로 토큰 소비량, 턴당 latency, API 요청 수, 세션과 코드 변경 카운트 같은 것을 방출합니다. 다만 신호(signal)가 하나로 뭉뚱그려지는 게 아니라 &lt;strong&gt;metrics / events(logs) / traces&lt;/strong&gt;로 구분되어 나오는데, Coding Agent Insights 대시보드가 사용하는 것은 이 중 &lt;strong&gt;metrics&lt;/strong&gt;입니다. 즉, 이 도구들은 CLI 겉모양은 단순해 보여도, 안쪽에서는 이미 관측 가능한(observable) 애플리케이션처럼 동작하도록 만들어져 있습니다. 환경 변수 몇 개만 세팅해주면 OTLP 프로토콜로 원하는 collector 혹은 백엔드로 신호를 보내줍니다.&lt;/p&gt;

&lt;p&gt;CloudWatch가 이 데이터를 받아들이는 방식이 흥미로웠습니다. 별도의 에이전트 없이 &lt;code&gt;https://monitoring.&amp;lt;region&amp;gt;.amazonaws.com/v1/metrics&lt;/code&gt; 형태의 네이티브 OTLP HTTP 엔드포인트를 열어두고, Bearer 토큰(AWS SigV4로도 가능) 인증만 통과하면 CloudWatch OTel Metrics로 수집합니다. 내부적으로는 이 메트릭이 CloudWatch의 OTel Metrics(신형 저장소)로 들어가서 PromQL로 조회 가능한 상태가 됩니다. 대시보드는 정해진 metric과 attribute shape을 기반으로 제공되는 managed dashboard로, 이 저장소를 미리 정의된 PromQL 쿼리로 훑어서 그려줍니다.&lt;/p&gt;

&lt;p&gt;Coding Agent Insights 대시보드가 "기본 제공된다"는 말에는 트릭이 있습니다. 대시보드가 기대하는 메트릭 이름(예: &lt;code&gt;claude_code.token.usage&lt;/code&gt;, &lt;code&gt;claude_code.cost.usage&lt;/code&gt; 등)과 차원(dimension)이 있고, 조직적 속성인 &lt;code&gt;user.email&lt;/code&gt;, &lt;code&gt;team.id&lt;/code&gt;, &lt;code&gt;department&lt;/code&gt;, &lt;code&gt;cost_center&lt;/code&gt; 같은 값은 반드시 OTel &lt;strong&gt;resource attribute&lt;/strong&gt;로 실려 와야 합니다. Metric attribute로 넣으면 대시보드의 슬라이스 필터에 잡히지 않습니다. 이 부분이 문서에서도 "Important"로 강조돼 있었는데, 실제로 처음에 metric attribute로 넣었다가 대시보드가 비어 보여서 한참 헤맸습니다.&lt;/p&gt;

&lt;p&gt;AWS 측 구성은 단순합니다. Node.js와 Claude Code CLI가 도는 환경(로컬 노트북이든 EC2든 무관)에서, IAM 역할 대신 &lt;strong&gt;CloudWatch Metrics API Key&lt;/strong&gt;(&lt;code&gt;cloudwatch:PutMetricData&lt;/code&gt; 등 지표 전송 권한을 가진 IAM 사용자로부터 생성) 형태의 Bearer 토큰을 발급해 사용했습니다. 이 토큰은 모델 추론에 쓰는 Bedrock API Key와는 별개이며, 텔레메트리 전송 경로 인증에만 쓰입니다. 팀 단위로 사용자 프로파일을 나눠서 각기 다른 resource attribute를 세팅한 뒤 세션을 돌려봤습니다. Collector를 별도로 두지 않고 CLI가 직접 CloudWatch OTLP 엔드포인트로 전송하도록 만든 것이 핵심 결정이었고, 그 이유는 뒤에서 다시 설명하겠습니다.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3lk7rdizd4q4w4ifz7ls.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3lk7rdizd4q4w4ifz7ls.png" alt="Image description2" width="799" height="356"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  (B) 주요 이점과 고려 사항
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc8l6lbhi7eu5hjrb7upa.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc8l6lbhi7eu5hjrb7upa.png" alt="Image description3" width="799" height="366"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;가장 먼저 눈에 띈 것은 대시보드가 이미 "쓸 만한" 카테고리로 나뉘어 있다는 점이었습니다. 토큰 사용량은 input/output/cacheRead/cacheCreation 네 가지 &lt;code&gt;type&lt;/code&gt;으로 자동 분리되고, 모델별(&lt;code&gt;model&lt;/code&gt; 차원), 사용자별, 팀별로 자유롭게 grouping이 됩니다. 테스트 계정에서 세 개의 가짜 팀(&lt;code&gt;platform&lt;/code&gt;, &lt;code&gt;data&lt;/code&gt;, &lt;code&gt;frontend&lt;/code&gt;)을 만들어 돌려봤더니, 어느 팀이 Sonnet 대비 Opus를 많이 쓰고 있는지, cache hit ratio가 어느 수준인지 그래프로 바로 보였습니다. 별도 ETL이나 대시보드 코딩이 전혀 필요 없었습니다. &lt;code&gt;claude_code.cost.usage&lt;/code&gt; 같은 비용 metric이 기본 제공되는 것도 FinOps 관점에서 유용했습니다.&lt;/p&gt;

&lt;p&gt;실제로 세 개의 가짜 팀(&lt;code&gt;platform&lt;/code&gt;, &lt;code&gt;data&lt;/code&gt;, &lt;code&gt;frontend&lt;/code&gt;)으로 세션을 돌려보니, 단발 세션에서는 Total Tokens 33.9K 수준이던 것이 팀별로 나눠 세 세션을 돌리자 99.1K / 3 users / 3 sessions로 집계됐고, &lt;code&gt;Group by Team&lt;/code&gt;으로 바꾸자 팀별 토큰 사용량이 색깔별 그래프로 바로 갈렸습니다. cache hit ratio는 서로 다른 짧은 프롬프트라 이번 테스트에선 0으로 나왔습니다(같은 긴 컨텍스트를 반복하면 올라갑니다).&lt;/p&gt;

&lt;p&gt;프롬프트 스타일과 사용량의 관계도 흥미로운 단서였습니다. 같은 리팩터링 요청이라도 "관련 파일을 먼저 찾아본 뒤 수정해줘"처럼 탐색을 유도하면 세션당 토큰과 turn 수가 늘고, 컨텍스트를 미리 붙여주면 줄어드는 경향이 metric에서 드러났습니다. 즉, 대시보드로 프롬프트 작성 방식과 사용량의 관계를 엿볼 수 있었습니다.&lt;/p&gt;

&lt;p&gt;한편 운영상 고려할 점도 몇 가지 있었습니다. 첫째, 개발자 로컬 머신에서 직접 CloudWatch로 전송하게 하려면 Bearer 토큰(CloudWatch Metrics API Key)을 배포해야 하는데, 이 장기 자격증명이 로컬 셸 환경에 남는 것을 어떻게 관리할지가 이슈입니다. 개인 실험은 단순히 &lt;code&gt;~/.zshrc&lt;/code&gt;에 넣었지만, 팀 규모라면 뒤에서 설명할 Collector + Instance Role(SigV4) 구조나 SSO 기반 enterprise 롤아웃 경로를 쓰는 게 맞습니다.&lt;/p&gt;

&lt;p&gt;비용 측면은 예상보다 신경 쓸 부분이 있었습니다. CloudWatch OTel Metrics는 unique time series 개수가 아니라 &lt;strong&gt;수집한 비압축 OTLP payload 용량(GB)&lt;/strong&gt;을 기준으로 과금됩니다. 따라서 &lt;code&gt;user.email&lt;/code&gt;, &lt;code&gt;team.id&lt;/code&gt;, &lt;code&gt;model&lt;/code&gt; 같은 attribute가 늘어도 시계열별 저장 요금이 직접 부과되지는 않습니다. 다만 attribute가 많아질수록 datapoint 크기가 커지고, PromQL query가 스캔하는 sample 수도 늘어날 수 있어, 카디널리티는 여전히 비용과 query 성능 측면에서 관리해야 합니다. 참고로 PromQL query 중 콘솔과 CloudWatch 대시보드에서 실행되는 것은 무료이고, PromQL alarm은 alarm 비용과 query 비용이 발생합니다.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9nal4a300bvrd5wnr9h4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9nal4a300bvrd5wnr9h4.png" alt="Image description4" width="799" height="354"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;마지막으로 대시보드가 만능은 아니라는 점을 짚어두고 싶습니다. 예를 들어 "어떤 파일을 주로 수정했는가", "어떤 언어에서 tool 실행이 실패하는가" 같은 세밀한 분석은 기본 대시보드에서는 불가능하고, 애초에 tool 실행 결과는 metrics가 아니라 event/log 신호라 별도 수집이 필요합니다. 다만 metrics 원 데이터가 PromQL로 그대로 열려 있기 때문에, Query Studio에서 커스텀 쿼리를 짜거나 CloudWatch dashboard에 붙이면 확장이 가능합니다. 정리하면, 기본 대시보드는 조직 전반의 adoption과 cost overview에 적합했고, PromQL 커스텀 뷰는 엔지니어링 리드용으로 나눠 쓰는 게 자연스러웠습니다.&lt;/p&gt;

&lt;h2&gt;
  
  
  시그널 구분: metrics / events / traces
&lt;/h2&gt;

&lt;p&gt;Claude Code의 OTel 신호는 섞어서 이해하면 안 됩니다. 다음처럼 구분됩니다.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Signal&lt;/th&gt;
&lt;th&gt;내용&lt;/th&gt;
&lt;th&gt;활성화&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Metrics&lt;/td&gt;
&lt;td&gt;토큰, 비용, 세션, 코드 변경 등&lt;/td&gt;
&lt;td&gt;&lt;code&gt;OTEL_METRICS_EXPORTER&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Events / Logs&lt;/td&gt;
&lt;td&gt;API 요청, tool result, tool decision 등&lt;/td&gt;
&lt;td&gt;&lt;code&gt;OTEL_LOGS_EXPORTER&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Traces&lt;/td&gt;
&lt;td&gt;Prompt → API → Tool 실행 관계&lt;/td&gt;
&lt;td&gt;별도 beta opt-in 필요&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;이 글의 설정은 metrics exporter만 활성화하므로 tool result event나 trace는 CloudWatch로 전송되지 않습니다. Coding Agent Insights 대시보드도 metrics 기반이라, tool 성공/실패율 같은 지표를 보려면 &lt;code&gt;OTEL_LOGS_EXPORTER&lt;/code&gt;와 CloudWatch Logs 수집을 추가로 구성해야 합니다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;프라이버시 기본값&lt;/strong&gt;도 짚어둘 만합니다. Claude Code 공식 문서 기준 기본적으로 프롬프트 원문, 어시스턴트 응답 원문, tool input과 파일 내용은 텔레메트리에 수집되지 않습니다. 상세 tool 내용과 raw body는 별도 opt-in이 필요합니다. 다만 OAuth 인증 시 &lt;code&gt;user.email&lt;/code&gt;은 telemetry에 포함될 수 있으니, 개인 식별자 노출을 원치 않으면 &lt;code&gt;team.id&lt;/code&gt;까지만 붙이는 절충안을 고려할 수 있습니다.&lt;/p&gt;
&lt;h2&gt;
  
  
  (C) 아키텍처 트레이드오프
&lt;/h2&gt;

&lt;p&gt;첫 번째 결정은 &lt;strong&gt;collector를 중간에 둘지, 에이전트가 직접 CloudWatch로 전송하게 할지&lt;/strong&gt;였습니다. 표준 OTel 프랙티스에 따르면 OpenTelemetry Collector를 EC2나 ECS Fargate에 띄워두고 각 개발자 머신은 그쪽으로 로컬 네트워크 송신을 하는 게 정석입니다. 하지만 코딩 에이전트의 실행 위치가 개인 노트북, 원격 devcontainer, GitHub Codespaces 등으로 분산돼 있어서 공통 접점을 만들기 어려웠습니다. 결국 이번 PoC에서는 CLI가 CloudWatch OTLP 엔드포인트로 직접 전송하는 방식을 택했고, 대신 개발자 조직 안에서 collector를 운영할 여력이 있다면 sampling/필터링을 collector에 넣는 게 장기적으로 낫다고 봤습니다.&lt;/p&gt;

&lt;p&gt;두 번째는 &lt;strong&gt;인증 방식&lt;/strong&gt;입니다. Bearer 토큰과 SigV4(IAM) 두 가지 옵션이 있는데, 로컬 CLI 환경에서는 Bearer 토큰이 훨씬 다루기 쉽습니다. IAM은 EC2/ECS 같은 AWS 내부 리소스에서 실행할 때 자연스럽지만, 개인 노트북에서 IAM 자격증명을 관리하려면 aws-vault 같은 도구가 추가로 필요합니다. 대신 Bearer 토큰(CloudWatch Metrics API Key)은 유효기간 관리와 회수 정책을 따로 세워야 한다는 부담이 남습니다. 이번 실험은 개인 계정이라 단순 Bearer로 처리했지만, 실제 조직 배포라면 SSO 기반 enterprise 경로를 강력히 추천합니다.&lt;/p&gt;

&lt;p&gt;세 번째는 &lt;strong&gt;resource attribute를 어디까지 표준화할지&lt;/strong&gt;입니다. 처음에는 &lt;code&gt;user.email&lt;/code&gt;, &lt;code&gt;team.id&lt;/code&gt;, &lt;code&gt;cost_center&lt;/code&gt;만 넣으려 했는데, 나중에 "특정 프로젝트 리포지토리에서 사용량이 얼마나 나오는지" 궁금해질 것 같아 &lt;code&gt;project.repo&lt;/code&gt;도 추가했습니다. 문제는 이런 커스텀 attribute는 기본 대시보드에는 노출되지 않고 PromQL 커스텀 쿼리에서만 쓸 수 있다는 점입니다. 대시보드 표준 슬라이스를 최대한 활용하려면 문서에 명시된 attribute set만 넣고, 그 외는 label sprawl로 이어질 수 있으니 자제하는 게 낫다는 결론이었습니다.&lt;/p&gt;

&lt;p&gt;네 번째는 &lt;strong&gt;개발자 프라이버시와 관측 가능성 사이의 균형&lt;/strong&gt;이었습니다. &lt;code&gt;user.email&lt;/code&gt;을 붙이면 개인 단위 분석이 가능해지지만, 조직에 따라서는 이걸 부담스러워하는 문화도 있습니다. 대안으로 &lt;code&gt;team.id&lt;/code&gt;까지만 붙이고 개인 식별자는 제외하는 방식도 테스트해봤는데, 개인 단위 랭킹만 포기하면 대시보드 대부분은 여전히 잘 동작했습니다. "누가 많이 쓰는가"보다 "어느 팀이 어떻게 쓰는가"에 초점을 맞춘다면 이 절충안이 실용적이었습니다.&lt;/p&gt;
&lt;h3&gt;
  
  
  코드 예시
&lt;/h3&gt;

&lt;p&gt;Claude Code가 CloudWatch OTLP 엔드포인트로 메트릭을 전송하도록 만드는 최소 환경 변수 세팅입니다. 핵심은 &lt;code&gt;OTEL_RESOURCE_ATTRIBUTES&lt;/code&gt;에 조직 속성을 실어 보내는 것, 그리고 Claude Code는 &lt;strong&gt;metrics 전용 환경 변수&lt;/strong&gt;에 &lt;code&gt;/v1/metrics&lt;/code&gt;까지 &lt;strong&gt;전체 경로&lt;/strong&gt;를 명시해야 한다는 점입니다. (범용 &lt;code&gt;OTEL_EXPORTER_OTLP_ENDPOINT&lt;/code&gt;에 base URL만 넣는 패턴은 Copilot 방식이고, Claude Code에는 맞지 않습니다.)&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Claude Code OTel 메트릭을 CloudWatch로 직접 전송&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;CLAUDE_CODE_ENABLE_TELEMETRY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;OTEL_METRICS_EXPORTER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;otlp
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;OTEL_EXPORTER_OTLP_PROTOCOL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http/protobuf

&lt;span class="c"&gt;# metrics 전용 변수 + /v1/metrics 전체 경로 (Claude Code는 경로를 자동으로 붙이지 않음)&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;OTEL_EXPORTER_OTLP_METRICS_ENDPOINT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"https://monitoring.ap-northeast-2.amazonaws.com/v1/metrics"&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;OTEL_EXPORTER_OTLP_METRICS_HEADERS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"Authorization=Bearer &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;CW_OTLP_TOKEN&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# (선택) export 주기 단축 - 짧은 세션에서 값이 빨리 뜨게&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;OTEL_METRIC_EXPORT_INTERVAL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2000

&lt;span class="c"&gt;# 리소스 속성 - 대시보드 슬라이스에 반드시 resource로 실려야 함&lt;/span&gt;
&lt;span class="c"&gt;# 주의: 값 사이에 공백이 들어가면 안 됨&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;OTEL_RESOURCE_ATTRIBUTES&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"service.name=claude-code,user.email=hong@example.com,team.id=platform,department=engineering,cost_center=cc-1024,organization=acme"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;EC2에서 여러 개발자 프로파일을 흉내내기 위해, 사용자별로 별도 셸 프로파일을 만들었습니다. 아래는 팀별 프로파일을 자동 생성하는 스크립트 일부입니다. (실제 Unix 사용자 계정이나 container를 생성하는 완전한 재현 스크립트는 아니고, resource attribute 분리 방식을 보여주기 위한 발췌입니다.)&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# 팀별 개발자 프로파일 생성 (PoC 시뮬레이션용 - 발췌)&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;TEAM &lt;span class="k"&gt;in &lt;/span&gt;platform data frontend&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  for &lt;/span&gt;USER &lt;span class="k"&gt;in &lt;/span&gt;alice bob&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
    &lt;/span&gt;&lt;span class="nv"&gt;PROFILE_FILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/home/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;USER&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TEAM&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/.claude_env"&lt;/span&gt;
    &lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;dirname&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PROFILE_FILE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PROFILE_FILE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;
export OTEL_RESOURCE_ATTRIBUTES="service.name=claude-code,user.email=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;USER&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;@acme.test,team.id=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TEAM&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;,department=engineering,cost_center=cc-&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TEAM&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;,organization=acme"
export OTEL_EXPORTER_OTLP_METRICS_ENDPOINT="https://monitoring.ap-northeast-2.amazonaws.com/v1/metrics"
export OTEL_EXPORTER_OTLP_METRICS_HEADERS="Authorization=Bearer &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;CW_OTLP_TOKEN&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;  &lt;span class="k"&gt;done
done&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;보안 주의&lt;/strong&gt;: 공용 EC2 환경에서 장기 CloudWatch Metrics API Key를 &lt;code&gt;/etc/profile.d/&lt;/code&gt; 같은 world-readable 평문 파일에 심는 방식(예전 v0.1 예제)은 권장하지 않습니다. 그 인스턴스에 로그인하는 모든 사용자가 토큰을 읽을 수 있기 때문입니다. 공용 서버라면 아래처럼 Collector가 EC2 Instance Role로 SigV4 인증하고, 사용자에게는 장기 키를 노출하지 않는 구조가 더 안전합니다.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fujf646fwigs99uacyr09.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fujf646fwigs99uacyr09.png" alt="Image description5" width="800" height="342"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Direct 전송을 유지해야 한다면 사용자별 credential 발급, 최소 권한, 정기 회전 및 폐기 절차를 반드시 갖춰야 합니다.&lt;/p&gt;

&lt;p&gt;PromQL로 팀별 시간당 토큰 소비량을 직접 조회하는 예시입니다. 기본 대시보드로 부족할 때 Query Studio에서 이렇게 확장합니다. 두 가지를 주의해야 합니다. (1) CloudWatch PromQL은 OTel metric 이름의 &lt;strong&gt;점(&lt;/strong&gt;&lt;code&gt;.&lt;/code&gt;&lt;strong&gt;) 표기를 그대로 유지&lt;/strong&gt;하므로 큰따옴표로 감싼 셀렉터를 씁니다. (2) resource attribute는 &lt;code&gt;@resource.&lt;/code&gt; 접두사 레이블로 매핑됩니다(예: &lt;code&gt;@resource.team.id&lt;/code&gt;). 언더스코어 형태의 &lt;code&gt;team_id&lt;/code&gt;로는 조회되지 않습니다.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# 팀별 최근 1시간 output 토큰 소비 총량 (Query Studio에서 실행)
# rate()는 초당 평균 증가율이라 "시간당 총량"과 안 맞음 → increase() 사용
sum by ("@resource.team.id", model) (
  increase({
    "claude_code.token.usage",
    type="output"
  }[1h])
)

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxnxg4o4q0gck6476uaif.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxnxg4o4q0gck6476uaif.png" alt="Image description6" width="800" height="384"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;참고: tool 실행 성공/실패율은 metric이 아니라 &lt;code&gt;claude_code.tool_result&lt;/code&gt; event/log로 제공됩니다. metrics-only 설정에서는 조회되지 않으므로, 이 글에서는 tool 실패율 예제를 다루지 않습니다. 필요하면 &lt;code&gt;OTEL_LOGS_EXPORTER&lt;/code&gt; + CloudWatch Logs 수집을 별도 절로 구성하세요.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;CloudWatch alarm으로 특정 팀의 토큰 소비가 급증할 때 알림을 받는 설정입니다. PromQL alarm은 &lt;code&gt;--metrics&lt;/code&gt;(Metric Math용) 필드가 아니라 &lt;code&gt;PromQLCriteria&lt;/code&gt;를 사용하며, &lt;strong&gt;비교 조건을 query 안에&lt;/strong&gt; 넣습니다.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# PromQL 기반 CloudWatch alarm 생성&lt;/span&gt;
aws cloudwatch put-metric-alarm &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--alarm-name&lt;/span&gt; &lt;span class="s2"&gt;"claude-code-token-surge-platform"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--alarm-description&lt;/span&gt; &lt;span class="s2"&gt;"Platform 팀의 최근 1시간 토큰 사용량 급증 감지"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--evaluation-criteria&lt;/span&gt; &lt;span class="s1"&gt;'{
    "PromQLCriteria": {
      "Query": "sum(increase({\"claude_code.token.usage\", \"@resource.team.id\"=\"platform\"}[1h])) &amp;gt; 500000",
      "PendingPeriod": 600,
      "RecoveryPeriod": 300
    }
  }'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--evaluation-interval&lt;/span&gt; 300 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--alarm-actions&lt;/span&gt; arn:aws:sns:ap-northeast-2:123456789012:cost-alerts &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; ap-northeast-2

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;blockquote&gt;
&lt;p&gt;참고: PromQL alarm 문법은 AWS CLI 버전에 민감하므로, 재현 시 최신 CLI 사용을 권장합니다(아래 테스트 환경표의 AWS CLI 버전 참고).&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  Environment &amp;amp; Final Checklist
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;값&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Region&lt;/td&gt;
&lt;td&gt;ap-northeast-2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Code&lt;/td&gt;
&lt;td&gt;2.1.220&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AWS CLI&lt;/td&gt;
&lt;td&gt;2.25.14&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compute&lt;/td&gt;
&lt;td&gt;Local&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Model 추론&lt;/td&gt;
&lt;td&gt;kiro-gateway 경유, 관측 경로와 독립&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Teams&lt;/td&gt;
&lt;td&gt;platform, data, frontend&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Telemetry&lt;/td&gt;
&lt;td&gt;Metrics only, direct OTLP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auth&lt;/td&gt;
&lt;td&gt;CloudWatch Metrics API Key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Test period&lt;/td&gt;
&lt;td&gt;1h&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;재현하려는 분들을 위한 체크리스트입니다.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] CloudWatch OTLP 엔드포인트용 Bearer 토큰(CloudWatch Metrics API Key) 또는 IAM 자격증명 준비&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;OTEL_EXPORTER_OTLP_METRICS_ENDPOINT&lt;/code&gt;에 &lt;code&gt;https://monitoring.&amp;lt;region&amp;gt;.amazonaws.com/v1/metrics&lt;/code&gt; 전체 경로 세팅&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;OTEL_EXPORTER_OTLP_METRICS_HEADERS&lt;/code&gt;에 &lt;code&gt;Authorization=Bearer &amp;lt;토큰&amp;gt;&lt;/code&gt; 세팅&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;OTEL_RESOURCE_ATTRIBUTES&lt;/code&gt;에 &lt;code&gt;user.email&lt;/code&gt;, &lt;code&gt;team.id&lt;/code&gt;, &lt;code&gt;department&lt;/code&gt;, &lt;code&gt;cost_center&lt;/code&gt;, &lt;code&gt;organization&lt;/code&gt; 포함 (값 사이 공백 없이)&lt;/li&gt;
&lt;li&gt;[ ] Claude Code라면 &lt;code&gt;CLAUDE_CODE_ENABLE_TELEMETRY=1&lt;/code&gt; 활성화&lt;/li&gt;
&lt;li&gt;[ ] 콘솔의 GenAI Observability → Coding Agent Insights에서 값 확인&lt;/li&gt;
&lt;li&gt;[ ] Query Studio에서 PromQL(&lt;code&gt;increase&lt;/code&gt;, &lt;code&gt;@resource.&lt;/code&gt; 접두사)로 커스텀 뷰 시험&lt;/li&gt;
&lt;li&gt;[ ] 시간당 토큰 alarm 하나 정도는 걸어두기 (비용 급증 방지)&lt;/li&gt;
&lt;li&gt;[ ] 테스트 후 API Key 폐기 절차 수행&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Lessons Learned
&lt;/h2&gt;

&lt;p&gt;가장 인상적이었던 것은 "관측 가능성 자체를 도구 벤더가 담당하는" 흐름이었습니다. Claude Code나 Codex 같은 도구가 OTel을 기본 탑재하는 시대에는, 이걸 어떻게 계측할지 고민할 필요 없이 "어디로 보낼 것인가"만 결정하면 됩니다. AWS는 여기서 표준 OTLP 엔드포인트를 그대로 열어두는 방식으로 참여했습니다. 다만 전송 규격은 OTLP를 쓰더라도 dashboard와 저장, query는 CloudWatch에 결합되므로, "완전한 벤더 중립"이라기보다 "전송 계층은 표준, 관측 계층은 관리형"이라는 균형으로 이해하는 게 정확합니다.&lt;/p&gt;

&lt;p&gt;한편, 조직 속성을 resource attribute로 넣어야 한다는 규약은 처음에는 사소해 보였지만, 실제로는 팀 전체가 지켜야 할 스키마 계약(schema contract)이라는 점이 인상 깊었습니다. 각 개발자가 임의로 attribute를 붙이기 시작하면 payload가 커지고 query 성능과 비용에 영향을 주므로, &lt;code&gt;.claude_env&lt;/code&gt; 같은 공용 프로파일을 조직 차원에서 관리하는 편이 안전합니다. 다음에 팀에 도입한다면 이 프로파일을 SSO 로그인 훅과 연결해서 자동 주입하는 방식을 시도해보고 싶습니다.&lt;/p&gt;

&lt;p&gt;마지막으로, 코딩 에이전트 관측 데이터는 단순한 비용 리포팅 이상의 가치가 있었습니다. 토큰 사용 패턴이나 cache hit ratio를 보고 있으면, 개발자가 프롬프트를 어떻게 구성하는지에 대한 힌트가 나옵니다. 이걸 조직 내 프롬프트 가이드나 코칭 자료로 되먹임할 수 있다면, 관측 데이터가 곧 생산성 개선 루프의 입력이 되는 그림이 그려집니다. 이 부분은 다음 번에 좀 더 깊게 파볼 예정입니다.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table width="100%"&gt;
   &lt;tbody&gt;
   &lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;div class="table-wrapper-paragraph"&gt;&lt;table width="100%"&gt;
   &lt;tbody&gt;
   &lt;tr&gt;
   &lt;td width="128"&gt;&lt;a href="https://www.linkedin.com/in/kyungmin-kim-95b497359/" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe1g6a032vh4fk23c2v5f.png" width="574" alt="Kyungmin Kim" height="640"&gt;&lt;/a&gt;&lt;/td&gt;
   &lt;td&gt;
&lt;strong&gt;Kyungmin (Lucas) Kim&lt;/strong&gt;&lt;br&gt;
&lt;small&gt;
주식회사 이테크시스템의 AWS Solutions Architect로서, 실무 중심의 서버리스 엔지니어링과 전략적 AI 전환(AX)의 가교 역할을 하고 있습니다. 단순한 시스템 구현을 넘어 지능적이고 자율적인 환경을 전략적으로 설계하는 데 집중하며, AWS 생태계의 지속적인 발전에 기여하고자, 실전에서 얻은 아키텍처 패턴과 기술적 통찰을 기술 커뮤니티와 적극적으로 공유하고 있습니다.
&lt;/small&gt;
&lt;/td&gt;
   &lt;/tr&gt;
   &lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table width="100%"&gt;
   &lt;tbody&gt;
   &lt;tr&gt;
   &lt;td width="128"&gt;&lt;a href="https://www.linkedin.com/in/donghee-kim-478a27297/" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzv56qthfb2ot3qobsmno.png" width="800" alt="Donghee Kim" height="1200"&gt;&lt;/a&gt;&lt;/td&gt;
   &lt;td&gt;
&lt;strong&gt;Donghee (Chad) Kim&lt;/strong&gt;&lt;br&gt;
&lt;small&gt;
주식회사 이테크시스템의 AWS Solutions Architect이자 테크 에반젤리스트입니다. 기업 고객이 AWS와 AI를 비즈니스에 효과적으로 도입할 수 있도록 '비즈니스 퍼스트' 관점의 아키텍처 설계와 보안 컴플라이언스를 고려한 클라우드 보안 컨설팅을 지원하고 있으며, AWS Summit 2026에서 AX 기반 상품 전략 플랫폼 구축 사례(PRT302-S) 발표 및 AWS 13x Certified (Golden Jacket) 자격을 보유하고 있습니다.
&lt;/small&gt;
&lt;/td&gt;
   &lt;/tr&gt;
   &lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

</description>
      <category>aws</category>
      <category>cloudwatch</category>
      <category>opentelemetry</category>
      <category>codingagent</category>
    </item>
    <item>
      <title>Bedrock AgentCore harness 로 에이전트 PoC 후기</title>
      <dc:creator>Kyungmin (Lucas) Kim</dc:creator>
      <pubDate>Fri, 10 Jul 2026 00:34:43 +0000</pubDate>
      <link>https://dev.to/janus/bedrock-agentcore-harness-ro-eijeonteu-poc-hugi-4060</link>
      <guid>https://dev.to/janus/bedrock-agentcore-harness-ro-eijeonteu-poc-hugi-4060</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhlnryp3mxlemxvqtsaii.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhlnryp3mxlemxvqtsaii.png" alt=" " width="799" height="446"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;그러던 차에 &lt;a href="https://aws.amazon.com/blogs/machine-learning/amazon-bedrock-agentcore-harness-is-now-generally-available-go-from-idea-to-production-grade-agent-in-minutes/" rel="noopener noreferrer"&gt;Amazon Bedrock AgentCore harness is now generally available&lt;/a&gt;라는 글을 읽었습니다. "에이전트가 프로덕션에서 돌아가기 위해 필요한 모든 배선을 두 개의 API 호출 뒤로 숨겨주는 managed abstraction"이라는 설명이 흥미로워서, 바로 AWS 계정에서 직접 만들어 테스트해봤습니다.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkgr94w9ri7pak04hpoce.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkgr94w9ri7pak04hpoce.png" alt=" " width="800" height="212"&gt;&lt;/a&gt;&lt;/p&gt;

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

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

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

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

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

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

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

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdm21c6xhkij64kg1wj60.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdm21c6xhkij64kg1wj60.png" alt=" " width="800" height="388"&gt;&lt;/a&gt;&lt;/p&gt;

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

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

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

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpizi2yw4iz0tq63jwcq4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpizi2yw4iz0tq63jwcq4.png" alt=" " width="784" height="369"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;다음은 가장 단순한 형태의 harness 생성 코드입니다. 모델은 기본값(Claude Sonnet)을 쓰고, 도구로 Code Interpreter와 Browser만 붙였습니다.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# harness 생성: 설정만으로 에이전트 정의
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;
&lt;span class="n"&gt;control&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bedrock-agentcore-control&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;region_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;us-west-2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;control&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_harness&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;harnessName&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;aws_analytics_agent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;executionRoleArn&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;arn:aws:iam::123456789012:role/MyHarnessRole&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;systemPrompt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AWS 운영 데이터 분석 어시스턴트입니다.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;
    &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;agentcore_code_interpreter&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;code_interpreter&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;agentcore_browser&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;browser&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="c1"&gt;# skills 미지정 시 비활성, memory 미지정 시 managed memory 자동 생성
&lt;/span&gt;    &lt;span class="n"&gt;skills&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;awsSkills&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;paths&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;core-skills/*&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
                                     &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;specialized-skills/operations-skills/*&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]}}],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# 전체 응답 확인 -&amp;gt; harnessArn, status 키 확인
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8slphspk0rj73py8tt00.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8slphspk0rj73py8tt00.png" alt=" " width="799" height="179"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;다음은 harness 호출 코드입니다.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;

&lt;span class="n"&gt;runtime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;bedrock-agentcore&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;region_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;us-west-2&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

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

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;stream&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;contentBlockDelta&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;contentBlockDelta&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;delta&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;end&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;flush&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;metadata&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;usage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;metadata&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;usage&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s"&gt;---&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;입력: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;inputTokens&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; / 출력: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;outputTokens&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; 토큰&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fux4561o8g24wpvr2c51k.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fux4561o8g24wpvr2c51k.png" alt=" " width="799" height="512"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;▲ InvokeHarness 한 번으로 리소스 조회 → PPT 생성 → S3 업로드까지 완료&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1ymac2sim6lfl473pzfg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1ymac2sim6lfl473pzfg.png" alt=" " width="799" height="164"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;▲ 지정 버킷에 PPT가 업로드된 모습&lt;/em&gt;&lt;/p&gt;

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

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

&lt;p&gt;메모리 동작도 인상적이었습니다. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqujnxya3a3v2he1phyhs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqujnxya3a3v2he1phyhs.png" alt=" " width="799" height="325"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;▲ 같은 Session ID로 후속 질문 — 이전 분석 결과를 정확히 기억하고 요약&lt;/em&gt;&lt;/p&gt;

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

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

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

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

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

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

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

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

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

&lt;p&gt;프로덕션에서 여러 harness가 같은 파일을 공유하거나, 양방향 동기화가 &lt;br&gt;
필요하다면 S3 Files access point를 검토하면 됩니다 (VPC 필수).&lt;br&gt;
프로덕션에서 최소 권한으로 좁힐 때의 참고 예시입니다.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Statement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Sid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"AgentCoreMemory"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"bedrock-agentcore:CreateEvent"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"bedrock-agentcore:RetrieveMemoryRecords"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:bedrock-agentcore:us-west-2:123456789012:memory/*"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Sid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"BedrockModelInvocation"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"bedrock:InvokeModel"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"bedrock:InvokeModelWithResponseStream"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:bedrock:us-west-2::foundation-model/*"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


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

&lt;p&gt;다음은 PoC에서 사용한 invoke 호출입니다. 세션 ID를 33자 이상으로 맞춰야 한다는 점만 주의하면 됩니다.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# invoke: 같은 sessionId로 다시 부르면 대화가 이어집니다
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt;
&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bedrock-agentcore&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;region_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;us-west-2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

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

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stream&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;contentBlockDelta&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;delta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;contentBlockDelta&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;delta&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;end&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;flush&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;세션 중간에 모델을 갈아끼우는 것도 시험해봤습니다. 같은 sessionId로 첫 턴은 Sonnet, 두 번째 턴은 Opus를 명시했더니 컨텍스트 손실 없이 매끄럽게 이어졌습니다.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# 같은 세션에서 모델만 바꿔 호출 - 컨텍스트 유지됨
&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invoke_harness&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;harnessArn&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;HARNESS_ARN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;runtimeSessionId&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bedrockModelConfig&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; 
           &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;modelId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;us.anthropic.claude-opus-4-5-20251101-v1:0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}},&lt;/span&gt;
    &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
               &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;방금 분석한 결과를 PPT 슬라이드 3장으로 정리해줘.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}]}],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


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

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

&lt;p&gt;복제하려는 분을 위한 체크리스트입니다.&lt;/p&gt;

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

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

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fko03rwtfknq6v6erudkj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fko03rwtfknq6v6erudkj.png" alt=" " width="800" height="366"&gt;&lt;/a&gt;&lt;/p&gt;

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

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

&lt;div class="table-wrapper-paragraph"&gt;&lt;table width="100%"&gt;
   &lt;tbody&gt;
   &lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;div class="table-wrapper-paragraph"&gt;&lt;table width="100%"&gt;
   &lt;tbody&gt;
   &lt;tr&gt;
   &lt;td width="128"&gt;&lt;a href="https://www.linkedin.com/in/kyungmin-kim-95b497359/" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe1g6a032vh4fk23c2v5f.png" width="574" alt="Kyungmin Kim" height="640"&gt;&lt;/a&gt;&lt;/td&gt;
   &lt;td&gt;
&lt;strong&gt;Kyungmin (Lucas) Kim&lt;/strong&gt;&lt;br&gt;
&lt;small&gt;
주식회사 이테크시스템의 AWS Solutions Architect로서, 실무 중심의 서버리스 엔지니어링과 전략적 AI 전환(AX)의 가교 역할을 하고 있습니다. 단순한 시스템 구현을 넘어 지능적이고 자율적인 환경을 전략적으로 설계하는 데 집중하며, AWS 생태계의 지속적인 발전에 기여하고자, 실전에서 얻은 아키텍처 패턴과 기술적 통찰을 기술 커뮤니티와 적극적으로 공유하고 있습니다.
&lt;/small&gt;
&lt;/td&gt;
   &lt;/tr&gt;
   &lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table width="100%"&gt;
   &lt;tbody&gt;
   &lt;tr&gt;
   &lt;td width="128"&gt;&lt;a href="https://www.linkedin.com/in/donghee-kim-478a27297/" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzv56qthfb2ot3qobsmno.png" width="800" alt="Donghee Kim" height="1200"&gt;&lt;/a&gt;&lt;/td&gt;
   &lt;td&gt;
&lt;strong&gt;Donghee (Chad) Kim&lt;/strong&gt;&lt;br&gt;
&lt;small&gt;
주식회사 이테크시스템의 AWS Solutions Architect이자 테크 에반젤리스트입니다. 기업 고객이 AWS와 AI를 비즈니스에 효과적으로 도입할 수 있도록 '비즈니스 퍼스트' 관점의 아키텍처 설계와 보안 컴플라이언스를 고려한 클라우드 보안 컨설팅을 지원하고 있으며, AWS Summit 2026에서 AX 기반 상품 전략 플랫폼 구축 사례(PRT302-S) 발표 및 AWS 13x Certified (Golden Jacket) 자격을 보유하고 있습니다.
&lt;/small&gt;
&lt;/td&gt;
   &lt;/tr&gt;
   &lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

</description>
      <category>aws</category>
      <category>bedrockagentcore</category>
      <category>agents</category>
      <category>strands</category>
    </item>
    <item>
      <title>CodeGraph를 EC2/EFS에 올리며 겪은 SQLite 쓰기 경합과 EventBridge 기반 실시간 동기화 해결기</title>
      <dc:creator>Kyungmin (Lucas) Kim</dc:creator>
      <pubDate>Mon, 01 Jun 2026 14:10:57 +0000</pubDate>
      <link>https://dev.to/janus/codegraphreul-ec2efse-olrimyeo-gyeoggeun-sqlite-sseugi-gyeonghabgwa-eventbridge-giban-silsigan-donggihwa-haegyeolgi-1ffm</link>
      <guid>https://dev.to/janus/codegraphreul-ec2efse-olrimyeo-gyeoggeun-sqlite-sseugi-gyeonghabgwa-eventbridge-giban-silsigan-donggihwa-haegyeolgi-1ffm</guid>
      <description>&lt;p&gt;며칠 전 &lt;a href="https://news.hada.io/topic?id=29873" rel="noopener noreferrer"&gt;CodeGraph - AI 코딩 에이전트를 위한 코드 지식 그래프&lt;/a&gt;라는 글을 읽었습니다. Claude Code나 Cursor 같은 AI 코딩 에이전트가 코드베이스를 탐색할 때 매번 grep과 Read를 반복하면서 토큰을 태우는 문제를, 미리 만들어둔 코드 지식 그래프로 해결하겠다는 접근이 흥미로웠습니다. 평균 도구 호출 71% 감소, 토큰 57% 감소, 비용 35% 절감이라는 벤치마크 수치도 눈에 띄었고요.&lt;/p&gt;

&lt;p&gt;CodeGraph는 한마디로 "코드베이스의 심볼,호출,구조를 SQLite에 인덱싱해두고, MCP(Model Context Protocol) 서버로 에이전트에게 질의 인터페이스를 제공하는 도구"입니다. 20개 이상의 언어를 Tree-sitter 기반으로 파싱하고, 파일 변경은 OS 네이티브 이벤트(FSEvents/inotify)로 따라잡습니다. 100% 로컬이라 외부 API 호출이 없는 것도 인상 깊었습니다.&lt;/p&gt;

&lt;p&gt;읽다 보니 "그럼 팀 단위로 공유하려면 어떻게 올려야 할까?"라는 의문이 생겼습니다. 로컬 실행이 기본인 도구를 AWS 위에 어떻게 자연스럽게 올릴지 고민하다가, EC2와 EFS 조합으로 PoC를 진행해봤습니다. 이 글은 그 여정을 정리한 기록입니다.&lt;/p&gt;

&lt;p&gt;테스트 대상 코드베이스는 약 12만 라인의 TypeScript/Python 혼합 모노레포로 잡았습니다. 평소 Claude Code로 작업하면서 "이 함수 어디서 호출해?" 같은 질문에 매번 grep이 4~5번 도는 게 답답했던 코드베이스입니다.&lt;/p&gt;

&lt;h2&gt;
  
  
  (A) 설계 의도와 아키텍처
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy1z6xedms3w6fs5x75qf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy1z6xedms3w6fs5x75qf.png" alt=" " width="800" height="421"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;먼저 CodeGraph 자체의 동작 원리부터 정리하면 이렇습니다. 핵심은 &lt;strong&gt;Tree-sitter로 AST를 만들고, 심볼,참조,호출 관계를 추출해 SQLite에 정규화된 그래프 형태로 저장&lt;/strong&gt;하는 것입니다. 노드는 함수/클래스/모듈 같은 심볼이고, 엣지는 호출(calls), 참조(references), 상속(extends) 같은 관계입니다. 여기에 SQLite의 FTS5(Full-Text Search) 가상 테이블을 얹어서 심볼명 검색을 즉시 처리합니다.&lt;/p&gt;

&lt;p&gt;기존 에이전트가 코드를 탐색할 때는 보통 Explore 서브 에이전트가 grep → 파일 후보 추리기 → Read로 본문 읽기 → 또 grep 하는 식으로 반복합니다. 이 과정 하나하나가 도구 호출이고 컨텍스트 윈도우를 갉아먹습니다. CodeGraph의 &lt;code&gt;smart_context&lt;/code&gt; 도구는 이걸 한 번의 호출로 압축합니다. "이 심볼과 관련된 진입점, 호출자, 피호출자, 그리고 실제 코드 스니펫"을 그래프 조인 한 방으로 반환합니다.&lt;/p&gt;

&lt;p&gt;또 하나 중요한 설계는 &lt;strong&gt;항상 최신 상태 유지(Always Fresh)&lt;/strong&gt; 입니다. 인덱스가 오래되면 에이전트가 잘못된 정보를 받게 되니까요. CodeGraph는 OS 네이티브 파일 시스템 이벤트(macOS의 FSEvents, Linux의 inotify)를 구독하고, 디바운싱을 걸어서 변경된 파일만 부분 재파싱합니다. 전체 재인덱싱이 아니라 델타만 갱신하기 때문에 큰 모노레포에서도 부담이 적습니다.&lt;/p&gt;

&lt;p&gt;이걸 AWS에 올리는 구도는 "EC2 1대 + EFS 마운트 + ALB 뒤에 MCP 엔드포인트" 형태로 잡았습니다. 컴퓨트는 t3.medium EC2(개인 PoC)에 ARM 기반 Graviton(t4g.medium)도 비교해봤습니다. 코드베이스는 EFS에 두고 여러 개발자가 동일한 인덱스를 공유합니다. CodeRepo는 CodeCommit이 아니라 GitHub을 그대로 두고, EventBridge로 푸시 이벤트를 받아 EC2 위 동기화 스크립트가 &lt;code&gt;git pull&lt;/code&gt;을 트리거하도록 했습니다. SQLite DB 파일도 EFS에 두긴 했지만 이 부분은 뒤에서 트레이드오프로 다시 다룹니다.&lt;/p&gt;

&lt;p&gt;CodeGraph의 핵심 동작을 개념적으로 단순화하면 다음과 같은 모양입니다. 실제 내부 스키마와는 다를 수 있지만, 심볼-관계 그래프의 구조를 이해하는 데는 이 정도면 충분합니다.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- 심볼 노드: 함수, 클래스, 모듈 단위&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;symbols&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;kind&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;              &lt;span class="c1"&gt;-- function, class, method 등&lt;/span&gt;
  &lt;span class="n"&gt;file_path&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;start_line&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;end_line&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- 관계 엣지: 호출, 참조, 상속&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;edges&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;src_id&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;dst_id&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;rel&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;               &lt;span class="c1"&gt;-- calls, references, extends&lt;/span&gt;
  &lt;span class="k"&gt;FOREIGN&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;src_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;symbols&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- FTS5: 심볼명 즉시 검색용 가상 테이블&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;VIRTUAL&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;symbols_fts&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="n"&gt;fts5&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'symbols'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  (B) Deep Dive: 주요 이점 및 고려 사항
&lt;/h2&gt;

&lt;p&gt;테스트 시나리오로 동일한 질문 10개를 두 번 던졌습니다. 첫 번째는 일반 Claude Code, 두 번째는 CodeGraph MCP를 붙인 Claude Code. 질문은 "X 함수의 영향 반경", "Y 라우트 핸들러가 호출하는 외부 서비스", "Z 클래스 계층" 같은 탐색형 질의였습니다. 결과는 도구 호출 수가 평균 11회에서 3.4회로 줄었고, 응답까지 걸린 시간도 체감으로 절반 정도였습니다. 글에서 주장한 70% 감소가 실제 체감과 크게 다르지 않았습니다.&lt;/p&gt;

&lt;p&gt;특히 인상적이었던 건 &lt;strong&gt;Impact Analysis&lt;/strong&gt; 도구였습니다. 실무에서 리팩토링을 할 때 가장 두려운 순간은 "이 함수 시그니처를 바꾸면 어디까지 깨지지?"를 판단하는 시점입니다. 보통은 grep으로 직접 호출처를 찾고, 그 호출처를 또 누가 호출하는지 한 단계 더 파고, 테스트 파일에서 이 함수를 모킹하고 있는 곳은 없는지 확인하는 식으로 3~4단계를 반복합니다. 코드베이스가 크면 이 과정에서 빠뜨리는 곳이 생기고, 그게 프로덕션 장애로 이어지기도 합니다. &lt;/p&gt;

&lt;p&gt;CodeGraph의 Impact Analysis는 그래프에서 N홉 BFS(너비 우선 탐색)를 돌려서 한 번에 영향 반경을 반환합니다. 예를 들어 formatDate라는 유틸 함수의 파라미터를 바꾸고 싶다면, "이 함수를 직접 호출하는 곳(1홉) + 그 호출자를 다시 호출하는 곳(2홉) + 관련 테스트 파일"을 한 번의 도구 호출로 트리 형태로 보여줍니다. 단순히 목록만 나열하는 게 아니라 호출 깊이와 파일 경로, 라인 번호까지 포함되어 있어서 에이전트가 바로 수정 계획을 세울 수 있었습니다. 변경 전 영향 분석에 들어가는 사고 비용이 확실히 줄었고, 무엇보다 "빠뜨린 곳이 없다"는 확신을 그래프 구조가 보장해준다는 점이 심리적으로 큰 차이였습니다.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flpayls64syz40kif0yxf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flpayls64syz40kif0yxf.png" alt=" " width="800" height="582"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;위 스크린샷처럼, codegraph impact 한 번이면 직접 호출처(1홉)부터 간접 호출처(2홉 이상)까지 트리로 펼쳐줍니다. 단순 텍스트 검색(grep)과 달리 호출 그래프 기반이라 런타임에 실제로 영향받는 심볼만 정확히 잡아내는 것이 핵심입니다.&lt;/p&gt;

&lt;p&gt;또 하나 체감이 컸던 건 smart_context의 코드 스니펫 반환 방식입니다. 기존에는 에이전트가 파일 전체를 읽어서 컨텍스트 윈도우를 낭비하는 경우가 많았는데, CodeGraph는 심볼의 시작 라인과 끝 라인을 알고 있으니 해당 함수 본문만 정확히 잘라서 반환합니다. 500줄짜리 파일에서 20줄짜리 함수만 필요한 상황이라면 컨텍스트 절약 효과가 극적입니다. &lt;/p&gt;

&lt;p&gt;반면 한계도 분명했습니다. 첫째, &lt;strong&gt;동적 호출과 메타프로그래밍에는 약합니다.&lt;/strong&gt; Python의 &lt;code&gt;getattr&lt;/code&gt; 기반 디스패치나TypeScript의 Proxy를 통한 호출은 정적 분석으로는 잡히지 않습니다. 그래프가 비어있는 게 아니라, "이 코드는 자주 직접 호출되지 않네?"라는 잘못된 신호를 줄 수 있습니다. 에이전트가 그래프를 과신할 때 오히려 위험할 수 있다는 뜻입니다. &lt;br&gt;
+실제로 테스트 중 데코레이터 패턴으로 감싸진 핸들러 함수가 그래프에서 "호출자 없음"으로 나타나는 케이스를 발견했고, 이런 경우에는 여전히 grep 기반 탐색이 보완적으로 필요했습니다.&lt;/p&gt;

&lt;p&gt;둘째, AWS 운영 관점에서는 &lt;strong&gt;SQLite 동시 쓰기 문제&lt;/strong&gt;가 가장 신경 쓰였습니다. EFS에 SQLite 파일을 두고 여러 EC2에서 마운트하면, EFS의 NFS 락 의미론과 SQLite의 파일 락이 충돌해서 쓰기 경합이 발생합니다. 결국 인덱서는 단일 EC2에서만 동작시키고, 다른 인스턴스는 읽기 전용으로만 EFS를 마운트하는 구조로 정리했습니다. WAL 모드를 켰지만 NFS 위에서는 권장되지 않아서 일반 rollback journal 모드로 되돌렸습니다.&lt;/p&gt;

&lt;p&gt;셋째, &lt;strong&gt;콜드 스타트 비용&lt;/strong&gt;입니다. 12만 라인 모노레포 첫 인덱싱이 t3.medium에서 약 6분 걸렸습니다. Graviton(t4g.medium)으로 옮기니 4분대로 줄었지만, 어쨌든 EC2를 끄고 켤 때마다 인덱싱을 다시 하면 안 되니 EFS에 DB를 영속화하는 게 필수였습니다. 그 대신 EFS의 IOPS 비용이 추가됩니다. PoC 한 달 비용을 따져보니 EC2(t4g.medium 24시간 운영) 약 $25, EFS(약 2GB + IOPS) $5 정도로 개인 부담은 가벼운 편이었습니다.&lt;/p&gt;

&lt;p&gt;넷째, &lt;strong&gt;MCP 서버를 외부에 노출할 때의 보안&lt;/strong&gt;입니다. CodeGraph는 100% 로컬을 전제로 만들어졌기 때문에 인증 레이어가 없습니다. 팀에 공유하려면 ALB 앞단에 OIDC 인증을 붙이거나, 최소한 VPN/Tailscale 같은 사설 네트워크 안에서만 접근하도록 막아야 합니다. 저는 PoC라 ALB + Cognito OIDC 조합으로 막아뒀습니다.&lt;/p&gt;
&lt;h2&gt;
  
  
  (C) 아키텍처 트레이드오프
&lt;/h2&gt;

&lt;p&gt;가장 먼저 고민한 건 &lt;strong&gt;Lambda vs EC2&lt;/strong&gt;였습니다. MCP 서버는 stdio 또는 long-lived HTTP 연결로 도는 게 자연스럽고, 파일 시스템 이벤트 구독이라는 동작 모델이 Lambda와 맞지 않았습니다. 파일 시스템 워처가 살아있어야 하니 상시 실행되는 컴퓨트가 필요했고, 결국 EC2를 택했습니다. ECS Fargate도 후보였지만, 파일 시스템 이벤트가 컨테이너 레이어 위에서 어떻게 동작할지 검증할 시간이 부족해서 단순한 EC2로 갔습니다.&lt;/p&gt;

&lt;p&gt;다음은 &lt;strong&gt;EFS vs EBS&lt;/strong&gt; 선택입니다. EBS가 IOPS도 좋고 SQLite 친화적이지만, 저는 "팀원이 자기 노트북에서도 동일한 코드베이스 마운트해서 보고 싶을 수 있다"는 시나리오를 가정했기 때문에 EFS를 골랐습니다. 대신 인덱서는 단일 라이터 정책을 강제했고, EFS Provisioned Throughput은 켜지 않고 Bursting 모드로 운영했습니다. 12만 라인 정도면 Bursting 크레딧으로 충분했습니다. 만약 코드베이스가 100만 라인 단위라면 EBS gp3 + AMI 스냅샷으로 가는 게 맞다고 봅니다.&lt;/p&gt;

&lt;p&gt;세 번째는 &lt;strong&gt;Git 동기화 방법&lt;/strong&gt;입니다. cron으로 5분마다 &lt;code&gt;git pull&lt;/code&gt;을 돌릴까, GitHub Webhook → API Gateway → Lambda → SSM Run Command로 이벤트 기반으로 갈까 고민했습니다. 결국 EventBridge + SSM 조합으로 갔습니다. 푸시가 없을 때는 아무 일도 일어나지 않고, 푸시가 있을 때만 EC2에서 &lt;code&gt;git pull&lt;/code&gt;이 실행되니 인덱서가 OS 이벤트를 자연스럽게 받아 부분 재인덱싱합니다. cron보다 깔끔했지만, 대신 처음 IAM 권한 잡는 데 시간이 좀 들었습니다.&lt;/p&gt;

&lt;p&gt;Neptune이나 OpenSearch 도입은 과도한 인프라 비용과 불필요한 리소스 낭비를 초래하는 오버엔지니어링이라 판단하여 배제했습니다.&lt;br&gt;
현재 규모에서 관리형 DB가 제공하는 확장성은 높은 유지 비용 대비 실질적인 ROI를 보장하지 못합니다. 콜드 스타트 없는 경량 아키텍처라는 본질에 집중하고 비용 효율성을 극대화하기 위해, SQLite 기반의 미니멈 아키텍처를 그대로 보존하는 전략을 채택했습니다.&lt;/p&gt;

&lt;p&gt;EC2 위에 CodeGraph를 설치하는 user-data 스크립트는 이렇게 단순합니다. 한 줄 설치를 표방하는 만큼 부트스트랩이 가볍습니다.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# EFS 마운트 (코드베이스 + DB 영속화 위치)&lt;/span&gt;
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /mnt/code
mount &lt;span class="nt"&gt;-t&lt;/span&gt; efs fs-0abc123:/ /mnt/code

&lt;span class="c"&gt;# CodeGraph 설치 (OS별 번들 자체 포함, Node.js 불필요)&lt;/span&gt;
curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://raw.githubusercontent.com/colbymchenry/codegraph/main/install.sh | sh

&lt;span class="c"&gt;# MCP 서버를 systemd 서비스로 상시 가동&lt;/span&gt;
&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt; &amp;gt; /etc/systemd/system/codegraph.service
[Service]
ExecStart=/usr/local/bin/codegraph mcp --workspace /mnt/code/repo
Restart=always
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;systemctl &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;--now&lt;/span&gt; codegraph
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;GitHub 푸시를 EC2 동기화로 연결하는 EventBridge 룰 설정은 이런 모양입니다. SSM Run Command를 타깃으로 잡았습니다.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"aws.partner/github.com/myorg"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"DetailType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"GitHub Push Event"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Targets"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Arn"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:ssm:ap-northeast-2::document/AWS-RunShellScript"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"RoleArn"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:iam::123456789012:role/EventBridgeSSMRole"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"RunCommandParameters"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"RunCommandTargets"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="nl"&gt;"Key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tag:Role"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"Values"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"codegraph"&lt;/span&gt;&lt;span class="p"&gt;]}]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Input"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;commands&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:[&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;cd /mnt/code/repo &amp;amp;&amp;amp; git pull&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;]}"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;MCP 클라이언트 쪽(예: Claude Code) 설정은 ALB 엔드포인트를 가리키도록 합니다. Cognito 인증을 붙였기 때문에 토큰을 헤더로 넘깁니다.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"codegraph"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://codegraph.internal.example.com/mcp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"headers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"Authorization"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bearer ${COGNITO_ID_TOKEN}"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Summary &amp;amp; Final Checklist
&lt;/h2&gt;

&lt;p&gt;핵심 결정을 요약하면 이렇습니다. &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;MCP 서버는 상시 실행이 필요하므로 EC2(가능하면 Graviton). &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;인덱스 DB는 EFS에 영속화하되, 단일 라이터 정책 유지. &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Git 동기화는 EventBridge + SSM으로 이벤트 기반. &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;외부 노출은 ALB + OIDC 인증으로 최소 보호. &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Neptune 같은 매니지드 그래프 DB로의 치환은 의도적으로 거부.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;원활한 PoC를 위한 체크리스트입니다. &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;EC2 보안 그룹은 ALB에서 오는 트래픽만 허용.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;EFS 보안 그룹은 EC2 SG에서 2049 포트만 허용. &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;SQLite는 NFS 위에서 WAL 모드 끄기.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;인덱서 EC2는 IMDSv2 강제, SSM 에이전트 활성화. &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Cognito 토큰 만료 시간을 짧게 잡고 클라이언트에서 갱신.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Conclusion: Lessons Learned
&lt;/h2&gt;

&lt;p&gt;가장 의외였던 건 "정적 분석 그래프 하나로도 에이전트의 행동 패턴이 이렇게 달라지나"였습니다. 그동안 RAG나 임베딩 기반 검색에만 관심이 쏠려 있었는데, 코드처럼 강한 구조가 있는 도메인에서는 임베딩보다 그래프가 더 정확한 컨텍스트를 적은 토큰으로 줄 수 있다는 걸 확인했습니다. 한국에서도 코드 어시스턴트 도입이 늘면서 토큰 비용이 슬슬 부담되는 시점인데, 이런 결정론적 인덱스 레이어가 다음 단계로 자연스럽게 보입니다.&lt;/p&gt;

&lt;p&gt;다음에 다시 한다면 두 가지를 바꾸겠습니다. &lt;br&gt;
첫째, EFS 대신 EBS gp3 + 일일 스냅샷으로 가서 SQLite의 동시성 이슈를 아예 회피하겠습니다. 팀 공유는 어차피 ALB 뒤 단일 인스턴스로도 충분합니다. &lt;br&gt;
둘째, 인덱서를 ECS Fargate로 옮기는 실험을 해보겠습니다. 파일 시스템 이벤트가 컨테이너 안에서 잘 작동하는지만 확인되면 운영 부담이 더 줄어들 것 같습니다.&lt;/p&gt;

&lt;p&gt;마지막으로 댓글에서 누군가 "그래프류가 너무 많아진다"고 했는데, 그 말도 일리는 있습니다. 다만 CodeGraph처럼 외부 의존성 없이 SQLite 한 파일로 끝나는 디자인은 PoC 비용이 거의 0에 가깝다는 게 큰 장점입니다. 일단 붙여보고 안 맞으면 떼면 그만이라는 점, 이게 도구 선택에서 의외로 중요한 요소라는 걸 다시 느꼈습니다.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>codegraph</category>
      <category>mcp</category>
      <category>ec2efs</category>
    </item>
  </channel>
</rss>
