AgentCore Runtime으로 원격 MCP 서버를 구축하고 수동 운영을 AI로 효율화한 경험 공유
요약
운영자가 수동으로 진행하던 CSV 파일 생성 및 업로드 과정을 Claude와 원격 MCP(Remote MCP) 기반을 활용하여 자동화한 경험을 공유합니다. AWS 환경에 아키텍처를 구축하고, 특히 AI가 사내 시스템에 접근할 때의 보안 경계 설정과 권한 관리에 초점을 맞추었습니다.
핵심 포인트
- 원격 MCP는 Claude가 호출하는 '사내 도구의 창구' 역할을 합니다.
- Cognito(Okta)와 JWT를 이용해 다중 계층에서 인증 및 권한을 검증했습니다.
- AI의 오작동 방지를 위해 읽기 작업은 SELECT 권한으로 제한했습니다.
- 전체 아키텍처는 AWS Bedrock AgentCore Runtime, VPC, ECS 등을 활용합니다.
서론
안녕하세요. 제가 AI에게 일을 시키는 입장에 놓이게 된 타로우 메가네입니다.
평소에는 Claside Corporation에서 레시차레의 백엔드 엔지니어로 일하고 있습니다.
레시차레에서는 운영자가 캠페인 설정용 CSV 파일을 만들어 관리 화면에서 업로드 → DB 업데이트와 같은 작업이 정기적으로 발생했고, 이로 인해 운영자들이 피로를 느끼고 있었습니다.
이를 'Claude 상에서 완결'할 수 있도록 AWS 위에 원격 MCP(Remote MCP) 기반을 구축하여 최근 출시했으며, 일정 수준의 효율화를 달성했기에 각 아키텍처 선정 이유와 어려웠던 점 등을 공유하고자 합니다.
보안 관련 설계 판단이 많은 글입니다. AI가 사내 시스템에 접근하게 할 때, 어디까지 접근시키고 어디서 멈출 것인가. 이 경계 설정에 초점을 맞춰 작성했습니다.
MCP가 필요했던 이유
원래의 운영 플로우는 다음과 같습니다. 운영자가 먼저 관리 화면에서 CSV 필수 헤더 등의 포맷을 확인하고, Claude를 이용해 목표하는 CSV 파일을 만든 다음, 완성된 파일을 다시 관리 화면을 열어 업로드합니다.
CSV 파일 자체를 만드는 것은 이미 Claude에게 맡겨져 있었지만, 그 전후에 관리 화면과의 왕래가 남아 있었습니다. 게다가 헤더 중에는 이름만으로는 의미 파악이 어려운 것도 있어, 어떤 열에 무엇을 넣어야 할지 매번 확인할 필요가 있었고, 이는 운영자 개인의 숙련도에 의존하는 부분이었습니다.
Claude가 MCP를 통해 포맷을 직접 참조하고 업로드까지 처리해 준다면, 이 왕래 과정 전체가 사라지고 Claude 화면 내에서 완결됩니다. 게다가 MCP 툴은 '각 헤더에 어떤 값이 들어갈지'라는 메타데이터까지 반환할 수 있어, 사람이 이름으로 추측하던 열의 의미를 AI는 정확한 컨텍스트로 받아들일 수 있습니다. 오히려 사람이 관리 화면을 보는 것보다 실수하기 어려울 것이라는 가설도 세웠습니다.
다만 운영자는 엔지니어가 아니기 때문에, 로컬에 MCP 서버를 구축하는 방식은 각자의 스킬과 환경에 따라 달라져 현실적이지 않았습니다. 각자 사용하는 Claude(웹/데스크톱)에 커스텀 커넥터를 등록하기만 하면 바로 사용할 수 있는 원격 MCP 서버가 필요하다고 생각했습니다.
쉽게 말해, 원격 MCP는 'Claude가 인터넷을 통해 호출할 수 있는 사내 도구의 창구'입니다. 창구이기 때문에 누가 들어올 수 있는지(인증)와 들어온 사람에게 무엇까지 할 수 있게 할지(권한)를 설계하는 것이 핵심이 됩니다.
전체 아키텍처
경로의 전체적인 모습은 다음과 같습니다.


인증은 Cognito(Okta 연동)를 사용하고, MCP 서버 실행 기반은 Amazon Bedrock AgentCore Runtime이며, 그 다음은 VPC 내부의 internal ALB를 거쳐 전용 ECS 상의 Rails에 도달합니다. MCP 서버 자체는 Python으로 만든 얇은 전송 계층이며, 비즈니스 로직과 권한 관리는 모두 애플리케이션 서버 측에 두었습니다.
핵심은 인증이 AgentCore 앞에서 완료되고, 검증된 JWT가 그대로 Rails까지 전달된다는 점입니다. Rails 쪽에서도 동일한 JWT를 독립적으로 검증하기 때문에, 중간의 어느 한 계층이라도 뚫려도 그것만으로는 끝까지 돌파할 수 없습니다.
AI의 폭주를 막기 위한 가드레일
AI가 사내 데이터에 접근하게 하는 것에서 가장 많이 고민한 부분이 바로 여기입니다. LLM은 악의도가 없어도 틀릴 수 있습니다. '잘못된 SQL을 실행하는', '잘못된 데이터를 운영 환경에 쓰는' 상황이 구조적으로 발생할 수 없도록 만들어야 했습니다.
가드레일은 두 가지로 구성되어 있습니다.
읽기는 SELECT 권한만 가진 DB 사용자에게 한정한다
MCP를 통한 데이터 참조용으로 전용 DB 사용자를 발급받았고, 부여된 권한은 SELECT에 국한했습니다. 연결 대상도 Aurora의 리더 인스턴스입니다. Claude가 아무리 폭주해도 DB에 할 수 있는 것은 '읽기'뿐이며, 쓰기/삭제/잠금(lock)은 권한 수준에서 불가능합니다.
프롬프트나 앱 코드의 기교로 막는 것이 아니라, DB의 GRANT 문으로 제한했습니다. LLM을 상대할 때는, LLM이 읽을 수 없는 계층에 제약을 두는 것이 가장 확실합니다.
쓰기는 인간의 승인을 거친다
Claude가 만든 CSV 파일은 운영 데이터에 직접 기록되지 않습니다. MCP 서버를 통해 얻은 서명된 URL로 S3 스테이징 버킷에 저장되며, 사람이 관리 화면에서 미리 보기 후 '승인' 버튼을 누른 것만 실제로 가져와집니다.
이 승인 플로우는 IAM으로도 강제했습니다. MCP 측의 태스크 역할(task role)은 S3 버킷에 대한 Put만 가능하고, 관리 화면 쪽의 역할은 Get만 가능한 비대칭적인 권한 설계입니다. 'AI가 만든 것은 인간이 확인해야 데이터가 된다'라는 워크플로우를 운영 규칙이 아닌 인프라 계층에서 구현했습니다.
각 AWS 서비스 선정 이유
경로를 위에서부터 순서대로 살펴보겠습니다.
Cognito + Okta
당사 사원 인증 기반은 Okta입니다. 따라서 핵심 구성은 'Okta만으로 완결되는' 구조여야 하며, Okta를 그대로 OAuth의 인가 서버(authorization server)로 사용할 수 있다면 등장인물을 늘리지 않아도 됩니다.
하지만 Okta 상에 인가 서버를 구축하는 기능은 계약 중인 플랜에 포함되어 있지 않았고, 이 용도를 위한 플랜 변경 역시 비용 대비 효율성이 좋지 않았습니다. 그렇다고 해서 인가 서버를 자체적으로 구축하는 것은 과했습니다. 토큰 발급 관련 부분을 직접 관리하지 않는 것이 이번의 일관된 방침입니다.
그래서 어쩔 수 없이 관리형(managed) 인가 서버로 Cognito를 끼워 넣었습니다. Cognito User Pool에 Okta를 IdP(Identity Provider)로 연동하여, 사용자 관리 및 퇴직 처리(offboarding)는 Okta에 통합하고, Cognito는 'OAuth 토큰 발급'에만 전념하는 구성입니다. Claude의 커넥터 인증을 위해 별도의 사용자 관리가 추가되는 일도 없고, 퇴직자의 계정을 Okta에서 중단시키면 MCP 접근도 동시에 차단됩니다.
트레이드오프(trade-off)도 있습니다. Okta 연동으로 로그인한 사용자는 Cognito User Pool에 페더레이티드(federated) 사용자로서 생성되며, Okta 측에서 권한을 제거해도 User Pool에는 남아있기 때문에 주기적인 정리 작업이 필요합니다. 하지만 Okta에서 권한이 없어지면 새로운 인증은 통과하지 못하며, 토큰의 유효 기간도 짧게 설정되어 있어 남아있는 토큰도 금방 만료됩니다. 심각한 문제는 아니라고 판단했습니다.
AgentCore Runtime
MCP 서버의 실행 기반으로는 Amazon Bedrock AgentCore Runtime을 사용했습니다. 간단히 말해 'MCP 서버 컨테이너를 배치하면, 공개 엔드포인트와 인증, 스케일링까지 모두 관리형으로 처리해주는 서비스'입니다.
ECS 등에서 자체 MCP 서버를 구축하는 대신 AgentCore를 선택한 이유입니다.
- 진입점(entry point)은 AWS가 관리하는
bedrock-agentcore.*.amazonaws.com을 사용하므로, 자체 공개 면이 없습니다. 공격 표면(attack surface) 관리를 계정 외부로 분리할 수 있습니다 - Cognito의 discovery URL과 client_id를 인바운드 JWT authorizer에 설정하는 것만으로 JWKS 획득, 서명 검증, 키 로테이션 추적까지 진입점에서 처리됩니다 - VPC 모드로 설정하면 Runtime이 private subnet에 ENI(Elastic Network Interface)를 생성하므로, '진입점은 인터넷(인증된 사용자만), 출구는 비공개망'이라는 비대칭적인 요구사항을 단 하나의 설정 블록으로 구현할 수 있습니다
- 과금은 처리 중인 vCPU/메모리 초 단위로만 이루어집니다. 운영자의 산발적인 이용 패턴에서는 ECS 상주보다 구조적으로 저렴하며, 초기 단계의 비용이 낮았습니다.
MCP 서버 본체는 Python + MCP 공식 SDK에 포함된 FastMCP로 구현했습니다. AgentCore 컨테이너 규약은 '0.0.0.0:8000/mcp에서 Streamable HTTP를 수신하는 것'뿐이기 때문에, 이를 충족하는 이미지를 ECR에 push하면 작동합니다.
from mcp.server.fastmcp import Context, FastMCP
mcp = FastMCP(
"retail-mcp",
...
구현의 핵심 포인트는 4가지입니다.
- 서버는 스테이트리스(stateless)합니다. MCP의 세션 관리는 Runtime 측에서 처리해주므로,
stateless_http=True를 선언하는 것만으로 충분합니다. 세션 스토어(session store)도 스티키 세션(sticky session)도 등장하지 않습니다 - - 툴(tool)의 실체는 전송(transfer)에 불과합니다.
_call_rails는 '수신한 Authorization 헤더를 그대로 붙여 Rails의/mcp/*API를 호출하는' 얇은 HTTP 클라이언트이며, 비즈니스 로직이나 권한 판정 기능을 갖지 않습니다. 모든 것은 Rails 측의 역할입니다 - - docstring이 프롬프트의 저장소입니다. 툴의 docstring은 그대로 툴 사양으로 Claude에게 전달됩니다. '먼저
list_csv_labels
「ヘッダー定義を確認する」といった機能を呼び出して確認すること、「ラベルを推測で作らないこと」といった使い方のガードはここに書いてあり、仮にClaudeが無視してもRails側が未知のラベルを400(Bad Request)で拒否する二重構造になっています。
エラーは返すものの、トークンは残しません。 Railsの4xx/5xx 에러는 오류 본문 전체를 Claude에게 반환합니다 (헤더 부족 등의 경우 Claude가 스스로 수정하여 재시도할 수 있기 때문에, 이것이 은근히 효과적입니다). 한편 CloudWatch Logs에는 Authorization 값과 CSV 본문을 일절 출력하지 않습니다.
internal ALB + 전용 ECS
AgentCore부터는 모두 VPC 내부에 있습니다. 내부(internal) ALB의 ingress는 Runtime 전용 보안 그룹으로부터만 허용하며, 경로는 /mcp*로 제한하고 헤드리스 체크 외에는 403으로 차단합니다. internal ALB라고 해서 VPC 내 어디에서든 도달할 수 있기 때문에, SG 참조를 통해 송신원을 하나로 한정했습니다.
ECS는 mcp 전용 서비스로 신설했습니다. 기존 관리 화면 서비스에 합치는 선택지도 있었지만, 분리함으로써 'MCP로부터의 접근'을 어느 계층에서도 식별할 수 있게 되었습니다. DB 사용자도 MCP 전용으로 발급했기 때문에, 슬로우 쿼리 로그나 프로세스 목록에 나열된 쿼리가 어떤 서비스에서 발생했는지 유저 이름만 보면 바로 알 수 있습니다. AI를 통한 쿼리는 인간의 조작과 달리 양이나 패턴을 예측하기 어렵기 때문에, '이 부하가 MCP인지, 관리 화면인지'를 즉시 구분할 수 있는 상태로 유지하는 것이 운영상 도움이 됩니다.
태스크 역할(Task Role)도 전용으로 사용하게 되므로, 가드 레일 섹션에서 작성한 'mcp는 Put만 / 관리 화면은 Get만'이라는 비대칭 IAM 역시 이 서비스 분리가 있어야만 설계할 수 있었습니다.
S3
CSV 업로드는 서명된 URL(Signed URL)을 얻어 운영자가 직접 PUT하는 방식입니다. 서명된 URL 발급은 Rails의 역할이며, MCP 서버 자체는 S3에 접근하지 않습니다. URL 유효 기간은 5분으로 짧게 설정되어 있어, 발급된 URL이 채팅 로그 등에 남아 있더라도 시간이 지나면 단순한 문자열이 됩니다.
스테이징 버킷(Staging Bucket) 운영은 'AI가 어지럽힐 것'이라는 전제에서 출발합니다. 대화의 시도와 오류 과정 속에서 결국 사용되지 않는 CSV 파일이 아무리 많이 생겨납니다. 이를 사람이 청소하는 방식으로 하지 않고, 라이프사이클 설정으로 기한이 되면 자동 삭제되게 합니다. 승인에 이르지 못한 파일은 그대로 두면 저절로 사라집니다.
반대로, 승인된 CSV는 이력으로 남기고 싶기 때문에, 승인 시점에 복사본을 별도로 생성하여 기록으로 보관합니다. '아무런 쓸모가 없었던 파일은 축적하지 않고, 채택된 파일은 사라지지 않게'라는 상태를 스테이징과 기록의 장소를 분리함으로써 성립시켰습니다.
배포 파이프라인
배포는 CodePipeline + CodeBuild로 진행되며, main에 병합(merge)될 때 MCP 서버 코드가 변경되었다면 자동으로 빌드 및 ECR push, AgentCore Runtime 업데이트까지 실행됩니다. 구성 자체는 일반적이므로 자세한 내용은 생략합니다.
Claude 측 커스텀 커넥터 설정
Claude의 커스텀 커넥터 설정도 필요합니다.
AgentCore Runtime의 URL과 Cognito에서 생성된 Client ID 및 Client secret을 등록합니다.
이렇게 하면 Okta로 허가된 사내 멤버는 커넥터를 통해 MCP에 쉽게 연결할 수 있게 됩니다.

실제로 사용하면 이렇게 됩니다
운영자에게 보이는 절차는 'Claude에 커넥터를 등록하고, 이후에는 대화만 하면 됩니다'.
최초 접속 시 Okta 로그인 화면으로 이동합니다. 이미 Okta 세션이 있다면 별다른 조작 없이 통과하므로, 체감상으로는 버튼을 한 번 누르는 것과 같습니다.
우선 어떤 도구들이 준비되어 있는지 물어봅니다.

다음으로 list_csv_labels로 CSV의 포맷을 확인합니다.

'다음 주 캠페인 설정용 CSV를 만들어줘'라고 요청하면, Claude가 MCP 도구를 사용하여 CSV 포맷 정의나 레이블 목록을 참조하며 CSV를 구성합니다. 각 헤더에 어떤 값이 들어갈지에 대한 메타데이터도 함께 반환되므로, 사람이 포맷 사양을 설명해 줄 필요가 없습니다.
내용이 확정되면 업로드용 서명된 URL을 얻어 CSV를 스테이징 영역에 배치합니다. 여기서부터는 사람의 차례로, 관리 화면의 미리보기에서 내용을 확인하고 승인해야만 실제 운영 데이터로 통합됩니다.
향후 전망
현재 권한은 'DB는 SELECT만 가능하고, 쓰기는 승인 화면을 통해서만 가능'이라는 가장 보수적인 설정에서 시작합니다.
하지만 사용하다 보면 당연히 '이 업데이트는 Claude에게서 직접 수행하게 해줬으면 좋겠다'라는 목소리가 나옵니다. 현재 팀에서 진행하고 있는 것은 MCP로부터의 쓰기를 허용하기 위한 규칙 설계입니다. 어떤 작업이라면 승인 화면을 거치지 않고 쓰기를 허용할지, 그 경우에 무엇을 로그로 남길지 등을 결정하는 것입니다. SELECT 권한만 가능하고 승인 화면이 필수라는 현재의 제약을 '왜 그것으로 안전한가'에 대한 근거를 재정립하면서 조금씩 완화해 나갈 예정입니다.
향후 동향은 저희 회사 테크 블로그에서도 지속적으로 올릴 계획이니, 클라실 주식회사(クラシル株式会社) 팔로우 부탁드립니다.
Happy MCP building! 🚀
Discussion

AI 자동 생성 콘텐츠
본 콘텐츠는 Zenn AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기