AI 에이전트에게 AWS 계정을 매핑하고 절감 보고서를 작성할 수 있는 충분한 접근 권한을 부여하는 방법, 그리고 그 이상.
제가 AI 에이전트가 실제 AWS 계정에서 디스커버리(discovery)를 수행하는 것을 보여줄 때 듣는 첫 번째 질문은 속도에 관한 것이 아닙니다. 그것은 바로 '접근 권한(access)'에 관한 것입니다.
“무엇을 부여했나요?”
이것이 맞는 질문입니다. 계정을 몇 분 만에 매핑하는 에이전트는 유용합니다. 하지만 다른 사람의 계정에서 무언가를 변경할 수 있는 에이전트는 위험 요소(liability)가 됩니다. 속도가 주목을 받게 합니다. 읽기 전용(read-only)인 부분이 이 모든 것을 안전하게 수행할 수 있게 만듭니다.
본 게시물은 시작 가이드 튜토리얼이 아닙니다. 이 글은 역할, 두 개의 MCP 서버, '읽기 전용'으로 조용히 처리되지 않았던 장소들, 그리고 제가 실제 청구서(bill)를 가리키자 에이전트가 발견한 내용에 대해 설명합니다. 핵심 코드는 본문에 포함되어 있으며, 전체 Terraform, Kiro config, 프롬프트 및 CloudTrail 확인 스크립트는 동반 리포지토리(https://github.com/dmernst/aws-readonly-discovery-agent)에서 확인할 수 있습니다.
하루 먼저 할 일: Cost Optimization Hub와 Compute Optimizer에 옵트인(opt in)하세요. 둘 다 무료이며, 각각 첫 번째 권장 사항을 생성하는 데 최대 24시간이 걸릴 수 있습니다.
아키텍처
- AWS MCP Server: 2026년 5월에 일반적으로 사용 가능해진 관리형 원격 서버입니다. 이 서버는 에이전트에게 격리된
aws___run_script 도구를 통해 전체 AWS API를 제공합니다.
- AWS Billing and Cost Management MCP server: AWS Labs의 오픈 소스 서버입니다. 이 서버는 Cost Explorer, Cost Optimization Hub, Compute Optimizer, Savings Plans, Budgets, 그리고 Price List API를 래핑(wraps)합니다.
Figure 1. 두 개의 MCP 서버, 하나의 읽기 전용 역할(read-only role), 그리고 기록을 위한 CloudTrail. 고객 조직은 그 위에 자체적인 가드레일(guardrail)을 추가합니다.
하나는 관리형이며 원격으로 작동하고, 다른 하나는 제 노트북에서 실행됩니다. 이 차이점은 중요하며, 나중에 다시 다루겠습니다.
읽기 전용 (Read-only)
사람들이 “읽기 전용 접근 권한을 주라”고 할 때, 그들은 보통 ReadOnlyAccess를 연결하고 끝내는 것을 의미합니다. 그것은 이 작업에 적합하지 않은 정책입니다. ReadOnlyAccess는 메타데이터뿐만 아니라 데이터까지 읽는 것을 포함합니다: s3:GetObject, DynamoDB 스캔, CloudWatch Logs 이벤트 등이 해당됩니다. 디스커버리 에이전트는 버킷이 존재하고 크기가 어느 정도인지만 알면 됩니다. 객체 자체를 열 필요는 없습니다.
읽기 전용은 에이전트가 환경의 내용물이 아니라 형태(shape)를 볼 수 있어야 함을 의미합니다.
따라서 가드레일은 네 개의 계층으로 구성되어 있으며, 각 계층은 다른 계층들이 놓치는 무언가를 포착합니다:
- IAM 역할: 메타데이터 읽기 권한과 범위가 지정된 비용 정책을 결합하고, 데이터 평면(data-plane) 읽기에 대한 명시적 거부(explicit deny)를 설정합니다.
- Kiro 승인: Autopilot 기능을 끄고, 모든 도구 호출 및 셸 명령어 실행 전에 제가 직접 승인합니다.
- Kiro 제어(steering): 에이전트가 무엇을 하는지, 무엇을 인용하는지, 그리고 언제 멈춰야 하는지에 대한 작성된 규칙입니다.
- CloudTrail: 사후 증거 자료입니다.
계층 1: IAM 역할 (The IAM role)
이 역할은 ViewOnlyAccess에서 시작합니다. 이는 콘텐츠를 읽지 않고 리소스를 나열하고 설명하기 위한 AWS 관리형 작업 기능 정책(job-function policy)입니다. 세션 시간은 한 시간으로 제한되며, 디스커버리 실행은 일주일 단위가 아니라 오후 단위의 작업입니다.
resource "aws_iam_role" "discovery" {
name = var.role_name
description = "AI 에이전트를 위한 읽기 전용 디스커버리 및 비용 분석"
...
}
resource "aws_iam_role_policy_attachment" "view_only" {
role = aws_iam_role.discovery.name
policy_arn = "arn:aws:iam::aws:policy/job-function/ViewOnlyAccess"
...
}
신뢰 정책(trust policy)은 누가 이 역할을 맡을 수 있는지 정확하게 명시하며, 파트너가 고객 계정으로 역할을 맡길 때 사용할 선택적 외부 ID(external ID)를 포함합니다:
data "aws_iam_policy_document" "trust" {
statement {
sid = "AllowNamedPrincipals"
...
principals {
type = "AWS"
identifiers = var.trusted_principal_arns
...
}
dynamic "condition" {
for_each = var.external_id == "" ? [] : [var.external_id]
content {
...
}
비용 권한: '읽기 전용'이 조용히 아닌 경우
청구 및 비용 관리(Billing and Cost Management) 서버의 README에는 필요한 권한 목록이 나열되어 있습니다. 대부분은 예상 가능한 내용입니다. ce:Get*, compute-optimizer:Get*, pricing:GetProducts 등이 그것입니다. 전체 목록을 읽어보면 s3:PutObject, s3:PutStorageLensConfiguration, athena:CreateWorkGroup, athena:CreateTable, 그리고 여러 ce:Start* 호출도 발견할 수 있습니다. 이 중 어느 것도 악의적이지 않습니다. Storage Lens가 Athena 결과를 기록할 장소가 필요하기 때문입니다. 하지만 README에 나열된 목록을 그대로 사용하면 '읽기 전용' 에이전트에게 S3와 Athena에 대한 쓰기(write) 접근 권한이 부여됩니다.
그래서 저는 이를 Get, List, Describe로 제한했고, 그 대가로 Storage Lens 분석 기능을 포기했습니다:
statement {
sid = "CostExplorerRead"
effect = "Allow"
...
나머지 비용 정책은 Cost Optimization Hub, Compute Optimizer (GetIdleRecommendations 포함), Savings Plans, Budgets, Pricing, Free Tier, 그리고 CloudWatch 메트릭에 대해 동일한 패턴을 따릅니다. 실제로는 아무런 비용이 들지 않았습니다. CloudTrail에는 에이전트가 발생시킨 31건의 청구 호출 기록이 남아 있으며, 거부된 호출은 없었습니다.
명시적 거부(The explicit deny)
마지막 구문은 데이터 평면 읽기(data-plane reads)를 거부합니다:
statement {
sid = "DenyDataPlaneReads"
effect = "Deny"
...
ViewOnlyAccess는 오늘날 대부분의 이러한 권한을 부여하지 않아야 합니다. 이 거부는 내일의 상황에 대비하는 보험과 같습니다. 즉, 관리형 정책 업데이트가 발생하거나, 팀원이 '이번 실행을 위해서만' 추가적인 정책을 연결할 수도 있습니다. 명시적 거부(explicit deny)는 모든 허용(allow)보다 우선합니다. (lambda:GetFunction은 응답에 함수 코드 다운로드 링크가 포함되기 때문에 여기에 있습니다.) 쓰기 권한에는 거부가 필요하지 않습니다. 역할 내 어느 것도 이를 허용하지 않기 때문입니다.
조건 키와 그들이 남기는 공백(The condition keys, and the gap they leave)
AWS MCP Server는 포워딩하는 모든 요청에 aws:ViaAWSMCPService와 aws:CalledViaAWSMCP라는 두 가지 조건 키(condition keys)를 추가합니다. 따라서 IAM 정책과 SCPs(Service Control Policies)가 에이전트 트래픽을 일반 사용자의 트래픽과 다르게 처리할 수 있게 합니다.
차이점은 다음과 같습니다. 키를 추가하는 것은 AWS 관리형 MCP 서버만 가능합니다. 비용 청구 서버는 로컬에서 실행되며, 제 프로필의 자격 증명(credentials)으로 Cost Explorer를 호출합니다. IAM 관점에서 볼 때, 이 호출들은 제가 직접 AWS CLI를 실행한 것처럼 보입니다.
조건 키는 좋은 2차 방어 계층입니다. 역할(role)이 최우선되어야 합니다.
배포하기 (Deploy it)
첫 번째 명령어 전에 두 가지 사항을 확인해야 합니다. 루트 사용자 사용 금지: 이 노트북에서 제가 처음 aws 로그인을 할 때 루트 사용자(root user)로 했었는데, 이 계정은 역할을 가정(assume a role)할 수 없고 정책으로 제한할 수도 없습니다. 그래서 배포를 위해 관리자 프로필이 있는 IAM 사용자를 생성했습니다. CLI의 제안 거부: 최근 버전의 aws configure는 'AI 코딩 에이전트(s)를 위한 AWS 스킬 및 AWS MCP 서버 구성'을 제안합니다. 이것은 사용자 레벨에 귀하의 관리자 프로필로 설치됩니다. MCP 설정은 워크스페이스에 있는 것만 되어야 합니다.
cd terraform
cp terraform.tfvars.example terraform.tfvars # trusted_principal_arns 설정
terraform init
...
플랜(plan)에는 하나의 역할, 하나의 정책 첨부(policy attachment), 그리고 두 개의 인라인 정책만 표시되어야 합니다. 다른 것이 있다면 멈추고 읽어보세요. 만약 'No valid credential sources found'로 플랜이 실패하면, AWS_PROFILE을 관리자 프로필로 설정하세요. 또한 직접 경계 테스트를 작성하는 경우, 임의의 ID에 대해 ec2 create-tags --dry-run은 건너뛰세요: EC2는 권한보다 리소스를 먼저 확인하기 때문에 InvalidVpcID.NotFound를 반환하며 아무것도 증명하지 못합니다. create-vpc --dry-run과 실제 s3api create-bucket이 깨끗하게 거부(denials) 결과를 보여줍니다.
2단계: 두 서버를 Kiro에 연결하기 (Wiring both servers into Kiro)
Kiro는 워크스페이스의 .kiro/settings/mcp.json에서 MCP 서버를 읽어오며, 이는 사용자 레벨 파일보다 우선합니다. 주의할 점이 두 가지 있습니다. .kiro/가 포함된 폴더 자체를 열어야 하며, 폴더를 신뢰(trust)해야 합니다. 그 후 Kiro를 완전히 종료하고 다시 시작하세요. 그렇지 않으면 로그에 'workspace scope skipped: workspace is not trusted and nothing starts'라고 표시됩니다.
SigV4를 프록시(proxy)를 통해 사용하면 두 서버 모두 하나의 프로필을 공유할 수 있어 CloudTrail에 단일 역할과 단일 세션 이름이 기록됩니다. 버전은 고정(pinned)되는데, 고객 계정에서는 무엇이 정확히 실행되었는지 알고 싶기 때문입니다. autoApprove는 비어 있고 Autopilot 기능은 꺼져 있으므로, Kiro는 모든 도구 호출 및 셸 명령어 전에 승인을 요청합니다.
그림 2. Autopilot 비활성화(오른쪽 하단): 모든 도구 호출 및 셸 명령어는 저의 승인을 기다립니다.
제가 질문한 것: “어떤 AWS 계정과 역할로 연결되어 있습니까? 각 MCP 서버에서 어떤 도구들을 가지고 있는지 목록을 나열하고, 그중 어느 것이 쓰기(write) 권한이 있는지 알려주세요.”
제 첫 번째 설정에는 --read-only라는 한 줄이 더 있었습니다. AWS의 설정 가이드에 따르면 이 플래그는 쓰기 기능이 있는 도구들을 숨긴다고 했고, 이는 제가 원하던 바와 정확히 일치하는 것 같았습니다. 스모크 테스트(smoke test)를 해보니 실제 동작은 다음과 같았습니다: aws-mcp가 여섯 개의 도구를 제시했지만, AWS API를 호출하는 유일한 도구인 aws___run_script는 사라졌습니다. 에이전트는 하루 종일 AWS에 대해 읽을 수는 있었지만, 제 계정의 리소스 하나도 나열할 수 없었습니다.
_그림 3. --read-only 사용 시: 여섯 개의 도구는 있지만, 그중 어느 것도 계정 정보를 볼 수 없습니다.*
이 플래그가 도구를 숨깁니다. 실제 어떤 도구가 무엇을 할 수 있는지는 역할(role)이 결정합니다.
그래서 저는 이 플래그를 제거했습니다. 이미 CLI에서 해당 역할을 테스트해 본 결과, ec2 create-vpc는 UnauthorizedOperation을 반환했고, s3api create-bucket은 AccessDenied를 반환했으며, ssm get-parameter는 ID 기반 정책(identity-based policy)에서 명시적인 거부(explicit deny)가 발생했습니다. 이제 에이전트는 쓰기 권한이 있는 도구를 볼 수 있게 되었고, 만약 실제로 시도하면 AWS가 호출을 거부합니다.
그림 4. 플래그가 없을 때: aws___run_script를 포함한 8개 도구
청구 서버에는 약 50개의 도구가 나열되어 있었고, Kiro는 그중 두 개(Savings Plans start* jobs)가 자체적으로 쓰기 권한을 가진 것으로 플래그를 지정했습니다. 해당 역할은 ce:Start*를 허용하지 않으므로, 둘 다 거부될 것입니다.
그림 5. 청구 서버에 대한 Kiro의 감사: 약 50개 도구 중 쓰기가 가능한 2개
스모크 테스트(smoke test)는 제가 계획하지 않은 경로도 보여주었습니다. 에이전트는 MCP 서버 어느 쪽에도 접근하기 전에 Kiro의 터미널에서 aws CLI를 실행했습니다. 이는 제가 기본 프로필을 유지하고 있지 않기 때문에 실패했을 뿐이며, --profile 플래그를 이용해 관리자 프로필로 접속하는 것은 역할을 완전히 건너뛰는 계정으로 들어가는 두 번째 경로입니다. 저는 이를 제어 규칙(steering rule)과 Autopilot 비활성화로 막았습니다.
레이어 3: 에이전트 제어(Steering the agent)
제어 파일(Steering files)은 에이전트가 모든 대화에 포함하는 Markdown 규칙입니다. IAM은 에이전트가 할 수 있는 것을 결정합니다. 반면, 제어는 에이전트가 해야 하는 것과 어떻게 보고할지 결정합니다.
---
inclusion: always
---
# Discovery and savings rules