CloudWatch Omni로 AWS와 Azure 장애를 한 곳에서 진단하는 Strands 운영 에이전트 만들기
CloudWatch Omni로 AWS와 Azure 장애를 한 곳에서 진단하는 Strands 운영 에이전트 만들기 : OIDC 연동부터 SQL 트레이스 분석까
AWS는 2026년 9월 22일 Amazon CloudWatch Omni를 정식 출시(GA)했습니다. CloudWatch Omni는 OpenTelemetry와 CloudWatch 스토리지를 기반으로 애플리케이션과 AI 에이전트의 텔레메트리를 하나의 Dataset에 모으고, 이를 SQL과 PromQL로 조회하는 관측성(Observability) 서비스입니다. 발표문에는 Strands Agents 같은 에이전트 프레임워크 지원과 Azure 워크로드 수집이 함께 소개되어 있습니다. 개념 설명은 공식 문서에 맡기고, 이 글에서는 버지니아 북부(us-east-1) 리전에서 직접 구성하며 어디까지 동작하는지 확인합니다.
이번에 만들 구성은 다음과 같습니다. Azure VM에서 실행되는 주문 API와 로컬에서 실행되는 Strands 운영 에이전트를 CloudWatch Omni 한 곳에서 관측하고, 이 에이전트가 Omni SQL과 Azure Monitor를 함께 조회해 장애를 진단합니다. 이 과정에서 Dataset Integration, Azure VM용 CloudWatch Agent의 OIDC 페더레이션, ADOT 기반 에이전트 자동 계측, SQL 텔레메트리 쿼리, Context Graph를 차례로 사용합니다. 각 기능을 어디에 사용했는지는 글 마지막에 표로 정리했습니다.
구성과 분석은 주로 CLI와 SDK(boto3의 cloudwatchomni 클라이언트)로 진행했고, 수집된 결과는 콘솔에서 Space를 만든 뒤 Omni 웹 UI에서도 함께 확인했습니다. Omni Agent와 Alert 기능은 이 글에서 다루지 않습니다.
실습 환경
| 항목 | 내용 |
|---|---|
| AWS 리전 | us-east-1 (버지니아 북부) |
| Azure 리전 | centralus, Ubuntu 22.04 VM 1대 (Standard_D2as_v5) |
| 로컬 런타임 | Python 3.14, strands-agents 1.57.1, aws-opentelemetry-distro 0.20.0, boto3 1.43.103 |
| 모델 | Amazon Bedrock us.anthropic.claude-sonnet-4-6 |
| 필요 권한 | AWS: IAM Role 생성 권한, Azure: 리소스 그룹 Owner (VM 생성, run-command 실행) |
| 사전 조건 | CloudWatch Transaction Search 활성화 (X-Ray 세그먼트 대상이 CloudWatchLogs) |
목표 아키텍처
멀티클라우드 환경에서 장애가 나면 “어느 콘솔부터 봐야 하는지” 판단하는 데 많은 시간이 듭니다. 그래서 이번 구성에서는 두 클라우드의 텔레메트리를 Omni Dataset 하나로 모았습니다. 운영 에이전트는 이 Dataset을 기준으로 분석하고, 필요할 때만 Azure 메트릭을 추가로 조회합니다.
Azure VM 안에는 주문 API와 CloudWatch Agent가 함께 설치되어 있습니다. 주문 API는 AWS 자격 증명 없이, 같은 VM의 127.0.0.1:4318로 OTLP 트레이스만 보냅니다. CloudWatch Agent는 VM의 System-assigned Managed Identity로 Azure 토큰을 발급받고, 이 토큰을 AWS STS AssumeRoleWithWebIdentity로 교환해 us-east-1로 텔레메트리를 전송합니다. 따라서 Azure 쪽에는 AWS Access Key 같은 장기 자격 증명이 저장되지 않습니다.
운영 에이전트는 이번 실습에서 로컬 PC로 실행했습니다. 공식 문서 기준으로는 Lambda, EC2, ECS, EKS, AgentCore Runtime 어디에 배포해도 같은 방식으로 계측할 수 있습니다. 에이전트 자신의 실행 기록(LLM 호출, 도구 호출, 토큰 사용량)도 ADOT를 통해 같은 Dataset에 저장되므로, 진단을 수행하는 에이전트 자체도 Omni에서 관측할 수 있습니다.

VM의 NSG(Network Security Group)에는 인바운드 규칙을 하나도 추가하지 않았습니다. 모든 통신은 VM에서 AWS로 나가는 아웃바운드 HTTPS이고, VM 관리는 az vm run-command로 처리했기 때문입니다.
장애 문의가 한 건 들어왔을 때의 처리 흐름은 다음과 같습니다.

마지막 화살표를 눈여겨볼 만합니다. 에이전트가 어떤 쿼리를 실행했고 토큰을 얼마나 썼는지도 같은 Dataset에 남기 때문에, 진단 결과를 나중에 검증할 수 있습니다.
Step 1. Omni Dataset 준비
Omni가 SQL로 조회하는 대상은 CloudWatch Dataset입니다. 로그와 트레이스를 Dataset으로 전달하는 연결이 필요한데, 이 역할을 Dataset Integration이 맡습니다. 이 단계에서는 콘솔 설정 대신 API로 Dataset Integration만 먼저 만듭니다.
두 가지 준비 방법을 비교하면 다음과 같습니다.
| 준비 방법 | 적합한 상황 | 명령 및 방법 |
|---|---|---|
| 콘솔 설정 | 웹 UI, Omni Agent, Alert까지 사용하려는 경우 | CloudWatch 콘솔 Settings에서 Omni Get started 선택 (Domain, Space, IAM Role 자동 생성) |
| API로 Dataset Integration 생성 | SQL 쿼리와 에이전트 연동만 먼저 검증하려는 경우 | aws observabilityadmin create-dataset-integration |
API 방식을 쓰려면 logs.amazonaws.com이 수임할 실행 Role을 먼저 만들어야 합니다. 아래 Trust Policy는 aws:SourceArn 조건으로 이 계정의 Dataset Integration만 Role을 사용할 수 있도록 제한합니다.
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": { "Service": "logs.amazonaws.com" },
"Action": "sts:AssumeRole",
"Condition": {
"StringEquals": { "aws:SourceAccount": "<ACCOUNT_ID>" },
"ArnLike": { "aws:SourceArn": "arn:aws:observabilityadmin:us-east-1:<ACCOUNT_ID>:dataset-integration/default" }
}
}]
}
Dataset Integration은 계정과 리전마다 하나만 만들 수 있고, ARN은 dataset-integration/default로 고정됩니다. 생성 직후 조회해보니 기존 aws/spans에 쌓여 있던 span도 Dataset에서 바로 보였습니다. 공식 문서에도 Space를 만들면 최근 최대 7일 치 로그와 트레이스를 backfill한다고 나와 있습니다.
| 증상 | 원인 | 조치 |
|---|---|---|
aws observabilityadmin에 create-dataset-integration 명령이 없음 | AWS CLI 버전이 오래됨 | CLI를 최신 버전으로 올리거나 boto3의 observabilityadmin.create_dataset_integration 사용 |
| SQL 결과가 계속 0건 | Dataset Integration이 없거나 Transaction Search가 꺼져 있음 | 위 두 명령으로 상태 확인 |
| Role 생성 직후 Integration 생성 실패 | IAM 변경 사항 전파 지연 | 10초 정도 기다린 뒤 재시도 |
이 단계를 마치면 “traces.default”, “logs.default” 테이블에 SQL을 실행할 수 있습니다.
Step 2. Azure VM의 CloudWatch Agent를 OIDC로 연결
Azure에서 AWS로 텔레메트리를 보내려면 AWS 인증이 필요합니다. IAM User의 Access Key를 VM에 넣으면 키 유출 위험이 생기고 주기적으로 교체해야 하는 부담도 있습니다. 그래서 Azure Managed Identity 토큰을 AWS 임시 자격 증명으로 교환하는 OIDC 페더레이션을 사용합니다. EKS의 IRSA와 같은 원리를 Azure VM에 적용한다고 생각하면 됩니다.
먼저 System-assigned Managed Identity를 켜고, 인바운드 포트를 열지 않은 상태로 VM을 생성합니다.
az vm create -g rg-cwomni-test -n vm-cwomni-app -l centralus \
--image Ubuntu2204 --size Standard_D2as_v5 \
--assign-identity [system] --nsg-rule NONE
AWS에서는 Azure 테넌트를 IAM OIDC Provider로 등록하고, 이 Provider를 신뢰하는 Role을 만듭니다.
aws iam create-open-id-connect-provider \
--url <https://sts.windows.net/><TENANT_ID>/ \
--client-id-list <https://management.azure.com/>
공식 문서의 Trust Policy 예시에는 aud 조건만 있습니다. aud만 검사하면 같은 테넌트의 다른 Managed Identity도 이 Role을 수임할 수 있습니다. 그래서 이번에는 sub 조건을 추가해 이 VM의 Managed Identity(Object ID)만 Role을 수임하도록 제한했습니다.
"Condition": {
"StringEquals": {
"sts.windows.net/<TENANT_ID>/:aud": "<https://management.azure.com/>",
"sts.windows.net/<TENANT_ID>/:sub": "<VM_MANAGED_IDENTITY_OBJECT_ID>"
}
}
Role에는 CloudWatchAgentServerPolicy를 연결합니다. 이어서 VM에 CloudWatch Agent를 설치하고, Role ARN을 환경 변수로 지정합니다.
CTL=/opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-ctl
sudo $CTL -a set-env -e CWAGENT_ROLE_ARN=arn:aws:iam::<ACCOUNT_ID>:role/CWOmniAzureVmAgentRole
sudo $CTL -a set-env -e AWS_REGION=us-east-1
sudo $CTL -a fetch-config -m auto -c default:otel -s
에이전트 로그에 Detected the instance is Azure VM과 Everything is ready가 찍히면 정상입니다. OTLP 수신 주소는 127.0.0.1:4317(gRPC)과 127.0.0.1:4318(HTTP)이라 VM 외부에서는 접근할 수 없습니다.
자격 증명이 전달되는 흐름을 정리하면 다음과 같습니다.

애플리케이션은 이 인증 과정에 전혀 관여하지 않습니다. 앱 코드와 설정 어디에도 AWS 관련 비밀값이 없습니다.
| 증상 | 원인 | 조치 |
|---|---|---|
region is required 오류로 에이전트가 멈춤 | AWS_REGION 미설정 (Azure VM에는 EC2 메타데이터가 없음) | set-env -e AWS_REGION=us-east-1 추가 후 fetch-config 다시 실행 |
| 로그에 EC2 메타데이터 400 오류가 찍힘 | Azure IMDS가 EC2 방식의 요청을 거절 | 이어서 Detected the instance is Azure VM이 나오면 무시해도 됨 |
| Role 수임 실패 | Trust Policy의 sub 값과 VM Managed Identity의 Object ID가 다름 | az vm show --query identity.principalId로 값 재확인 |
이 단계를 마치면 VM 안의 어떤 프로세스든 로컬 OTLP 주소로 보내기만 하면 데이터가 Omni까지 전달됩니다.
Step 3. Azure 애플리케이션을 OpenTelemetry로 계측
이 단계에서는 Omni에서 조회할 Azure 워크로드를 만듭니다. 결제 게이트웨이 타임아웃(10%)과 DB 지연(15%)을 일부러 섞은 주문 API 시뮬레이터입니다. 멀티클라우드 쿼리에서 클라우드를 구분할 수 있도록 Resource 속성에 cloud.provider=azure를 지정했습니다.
핵심 코드는 다음과 같습니다.
resource = Resource.create({
"service.name": "azure-order-api",
"deployment.environment.name": "blog-demo",
"cloud.provider": "azure",
"cloud.region": "centralus",
})
provider = TracerProvider(resource=resource)
# 같은 VM의 CloudWatch Agent로만 전송 (AWS 자격 증명 불필요)
provider.add_span_processor(BatchSpanProcessor(
OTLPSpanExporter(endpoint="<http://127.0.0.1:4318/v1/traces>")))
요청 span(POST /orders) 아래에 DB client span(inventory-db SELECT, db.system=postgresql)을 두고, 타임아웃이 나면 span 상태를 ERROR로 기록합니다. 앱을 실행하고 약 2분 뒤 조회하니 azure-order-api span 360건과 에러 13건이 Dataset에 들어와 있었습니다.
Omni 웹 UI의 Services 화면을 열면 별도 설정 없이 azure-order-api가 서비스로 자동 등록되어 있습니다. 아래 화면은 최근 30분 기준으로, 요청 779건 중 78건이 에러(에러율 10.0%)로 집계되었고 Hosted in 항목에 azure_vm이 표시되어 있습니다. 앱 코드에 넣은 타임아웃 비율(10%)과 정확히 일치합니다.

화면 상단의 Infrastructure distribution에 Database가 1건 잡힌 것도 눈여겨볼 만합니다. DB client span의 db.system=postgresql 속성만으로 Omni가 DB 리소스를 인식한 것입니다.
이 단계를 마치면 Azure 워크로드를 AWS 워크로드와 같은 테이블과 같은 화면에서 확인할 수 있습니다.
Step 4. Strands 운영 에이전트 작성과 ADOT 계측
이 단계에서는 장애 문의를 받으면 두 클라우드를 모두 조회하는 에이전트를 만듭니다. 도구는 세 개이며 모두 읽기 전용입니다.
| 도구 | 조회 대상 | 인증 | 권한 |
|---|---|---|---|
omni_sql | CloudWatch Omni Dataset (트레이스, 로그) | 실행 환경의 AWS 자격 증명 | 읽기 전용 (SELECT, WITH, EXPLAIN만 허용) |
azure_vm_inventory | Azure Resource Manager (VM 목록, 전원 상태) | AzureCliCredential | 읽기 전용 |
azure_vm_metrics | Azure Monitor 플랫폼 메트릭 | AzureCliCredential | 읽기 전용 |
omni_sql은 boto3의 cloudwatchomni 클라이언트로 Query Session을 열고 StartTelemetryQuery와 GetTelemetryQueryResults를 호출합니다. LLM이 생성한 SQL을 그대로 실행하는 구조이므로, 도구 쪽에서 허용할 문장 종류를 제한하고 반환 행 수도 잘라냅니다.
_client = boto3.client("cloudwatchomni", region_name="us-east-1")
_READ_ONLY = re.compile(r"^\s*(SELECT|WITH|EXPLAIN)\b", re.IGNORECASE)
@tool
def omni_sql(sql: str) -> dict:
"""CloudWatch Omni Dataset에 읽기 전용 SQL을 실행합니다. ({WINDOW_1H}는 최근 1시간 조건으로 치환)"""
sql = sql.replace("{WINDOW_1H}", time_window(1))
if not _READ_ONLY.match(sql) or ";" in sql.strip().rstrip(";"):
return {"error": "read-only single statement only"}
qid = _client.start_telemetry_query(sessionId=_session(), queryString=sql)["queryId"]
return wait_for_results(qid, max_rows=200) # GetTelemetryQueryResults 폴링
에이전트 정의는 간단합니다. System Prompt에는 “리소스를 변경하지 않는다”와 “답변 끝에 근거 목록을 붙인다”를 명시했습니다.
agent = Agent(
name="multicloud-ops-agent",
model=BedrockModel(model_id="us.anthropic.claude-sonnet-4-6", region_name="us-east-1"),
system_prompt=SYSTEM,
tools=[azure_vm_inventory, azure_vm_metrics, omni_sql],
trace_attributes={"session.id": "blog-demo", "team": "sre"},
)
에이전트 계측은 코드 수정 없이 ADOT 자동 계측으로 처리합니다. Strands에는 OpenTelemetry 트레이싱이 내장되어 있어서, aws-opentelemetry-distro를 설치하고 opentelemetry-instrument로 실행하기만 하면 됩니다. 공식 문서도 이 방식을 권장합니다.
pip install strands-agents "aws-opentelemetry-distro>=0.20.0"
export AGENT_OBSERVABILITY_ENABLED=true AWS_GENAI_CONTENT_EXTRACTION_OPT_OUT=true
export OTEL_PYTHON_DISTRO=aws_distro OTEL_PYTHON_CONFIGURATOR=aws_configurator
export OTEL_RESOURCE_ATTRIBUTES="service.name=multicloud-ops-agent,deployment.environment.name=blog-demo"
export OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf OTEL_TRACES_EXPORTER=otlp
export OTEL_LOGS_EXPORTER=none OTEL_METRICS_EXPORTER=none
opentelemetry-instrument python agent/ops_agent.py "질문"
AWS_GENAI_CONTENT_EXTRACTION_OPT_OUT=true는 프롬프트와 응답 본문을 span에 기록하지 않도록 하는 설정입니다. 운영 데이터를 다루는 에이전트라면 켜두는 것이 안전합니다.
실습하면서 막혔던 부분은 다음과 같습니다.
| 증상 | 원인 | 조치 |
|---|---|---|
span 전송 시 400 Bad Request | OTEL_EXPORTER_OTLP_TRACES_HEADERS로 커스텀 로그 그룹을 지정했지만, 해당 로그 그룹에 Resource Policy가 없음 (응답 본문에 logs:PutLogEvents 거부 메시지) | 헤더를 빼고 기본 aws/spans로 보내거나, 커스텀 로그 그룹에 Resource Policy 추가 |
ConflictException: session already exists | Query Session 이름은 계정 안에서 고유해야 함 | 세션 이름에 무작위 접미사를 붙이고, 도구가 병렬 호출될 때를 대비해 lock으로 한 번만 생성 |
| Windows Git Bash에서 로그 그룹 이름이 깨짐 | /aws/... 값을 Git Bash가 파일 경로로 자동 변환 | MSYS_NO_PATHCONV=1 설정 |
azure-monitor-query에서 메트릭 클래스 import 실패 | 2.0 버전부터 메트릭 API가 별도 패키지로 분리됨 | azure-mgmt-monitor의 metrics.list 사용, interval="PT5M"처럼 문자열로 지정 |
이 단계를 마치면 에이전트가 질문 하나로 Omni와 Azure를 오가며 데이터를 조회하고, 그 과정 전체가 트레이스로 남습니다.
Step 5. Omni SQL로 두 클라우드를 한 번에 조회
에이전트에게 맡기기 전에, 어떤 쿼리가 실제로 동작하는지 직접 확인합니다. Omni SQL에서는 @timestamp 범위 조건이 필수인데, 이 조건을 어떤 형식으로 쓰느냐에서 처음 막혔습니다.
| 시도한 조건 | 결과 |
|---|---|
`@timestamp` >= NOW() - INTERVAL '1' DAY | 실패: 조회 범위가 365일을 넘는다는 오류 |
`@timestamp` BETWEEN <나노초 정수> AND <나노초 정수> | 실패: 타입 변환 오류와 함께 to_timestamp()를 쓰라는 안내 |
`@timestamp` BETWEEN to_timestamp('2026-09-27T07:00:00Z') AND to_timestamp('2026-09-27T08:00:00Z') | 성공 |
API로 쿼리할 때는 to_timestamp()로 범위를 지정하는 방식이 가장 안정적이었습니다. 에이전트 도구에 {WINDOW_1H} 치환 기능을 넣은 것도 이 때문입니다.
먼저 클라우드별, 서비스별로 span 수와 에러 수를 집계합니다.
SELECT resource['attributes']['service.name'] AS service,
resource['attributes']['cloud.provider'] AS cloud,
COUNT(*) AS spans,
COUNT(*) FILTER (WHERE status['code'] = 'ERROR') AS errors
FROM "traces.default"
WHERE `@timestamp` BETWEEN to_timestamp('<START>') AND to_timestamp('<END>')
GROUP BY 1, 2
ORDER BY spans DESC
쿼리 한 번으로 Azure의 azure-order-api(cloud=azure), 로컬에서 실행한 multicloud-ops-agent, 계정에 원래 있던 AWS Lambda 서비스가 함께 조회되었습니다. span 상태 값은 UNSET과 ERROR 두 가지로 저장되어 있었습니다.
같은 테이블에서 AI 에이전트의 토큰 사용량도 집계할 수 있습니다. ADOT가 LLM 호출 span에 gen_ai.usage.* 속성을 기록하기 때문입니다.
SELECT name,
COUNT(*) AS calls,
SUM(CAST(attributes['gen_ai.usage.input_tokens'] AS BIGINT)) AS in_tok,
SUM(CAST(attributes['gen_ai.usage.output_tokens'] AS BIGINT)) AS out_tok
FROM "traces.default"
WHERE {WINDOW_1H} AND resource['attributes']['service.name'] = 'multicloud-ops-agent'
GROUP BY name ORDER BY calls DESC
에이전트를 5번 실행하는 동안 chat us.anthropic.claude-sonnet-4-6 span이 13건 기록되었고, 입력 토큰 21,087개와 출력 토큰 5,070개가 집계되었습니다. execute_tool omni_sql, execute_tool azure_vm_metrics 같은 도구 호출 span과, 도구 내부에서 발생한 CloudWatchOmni.StartTelemetryQuery 같은 AWS SDK span도 하나의 트레이스로 연결되었습니다.
지연 분석에는 approx_percentile_cont 함수를 사용합니다. azure-order-api의 POST /orders와 inventory-db SELECT의 p99가 모두 약 1,415ms로 나왔습니다. 요청 지연 대부분이 DB 구간에서 발생한다는 사실을 바로 확인할 수 있었습니다.
같은 서비스를 웹 UI의 서비스 상세 화면에서 열면, SQL로 계산한 값과 비슷한 수준의 P99 Request Latency(약 1.5초)가 그래프로 표시됩니다. Identity 영역에는 Cloud provider가 azure로, Account ID에는 AWS 계정이 아닌 Azure 구독 ID가 표시됩니다. Omni가 Azure 리소스를 AWS 계정과 구분해서 관리한다는 것을 알 수 있습니다.

마지막으로 Context Graph API를 cloudProvider=azure 조건으로 호출했습니다.
SELECT name,
COUNT(*) AS calls,
SUM(CAST(attributes['gen_ai.usage.input_tokens'] AS BIGINT)) AS in_tok,
SUM(CAST(attributes['gen_ai.usage.output_tokens'] AS BIGINT)) AS out_tok
FROM "traces.default"
WHERE {WINDOW_1H} AND resource['attributes']['service.name'] = 'multicloud-ops-agent'
GROUP BY name ORDER BY calls DESC
별도 설정을 하지 않았는데도 azure-order-api 서비스 노드(region=centralus, stage=blog-demo)와, 이 서비스가 CALLS 관계로 호출하는 postgresql 리소스 노드(category=DATABASE)가 반환되었습니다. span의 db.system 속성만으로 서비스 간 의존성이 자동으로 만들어진 것입니다.
이 단계를 마치면 “Azure 앱의 DB 지연”과 “에이전트의 토큰 비용”을 같은 쿼리 언어로 분석할 수 있습니다.
Step 6. 에이전트 진단 실행과 검증 루프
이 단계에서는 에이전트에게 실제 장애 문의를 맡기고, 결과를 Omni 데이터로 다시 검증합니다. 진단할 상황을 만들기 위해 VM에 약 15분 동안 CPU 부하를 걸어두었습니다.
에이전트에게 보낸 요청은 다음과 같습니다.
azure-order-api에서 결제 에러가 난다는 제보가 있어. 최근 1시간 Omni 트레이스로
에러율과 p99 지연, 느린 구간을 찾고, 그 앱이 도는 rg-cwomni-test Azure VM의 CPU도
같이 봐서 원인이 인프라인지 애플리케이션인지 판단해줘
에이전트는 Omni SQL로 에러 span(모두 HTTP 504), p99 지연, 분 단위 에러 타임라인을 조회했습니다. 이어서 Azure Monitor에서 VM CPU가 약 15분 동안 99.3%를 유지한 구간을 찾아내 표와 근거 목록으로 정리했습니다. 두 클라우드의 데이터를 연결해 분석하는 과정은 기대한 대로 동작했습니다.
하지만 “CPU 포화가 에러를 유발했다”는 결론은 틀렸습니다. 에러는 앱 코드가 무작위로 만든 것이고, CPU 부하는 앱을 시작하기 전부터 걸려 있었습니다. 에이전트가 시간대가 겹친다는 이유만으로 인과관계를 추론한 것입니다. 그 전 실행에서도 문제가 있었습니다. Dataset Integration을 만들기 전이라 조회 결과가 0건이었는데, 에이전트는 이를 “에러 없음, 정상”으로 보고했습니다.
이 두 가지 오판을 바탕으로 System Prompt를 다음과 같이 보완했습니다.
[System Prompt 추가]
- 조회 결과가 0건이면 '정상'이 아니라 '데이터 없음'으로 보고합니다.
- 에러 span은 status['code'] = 'ERROR', 지연은 (endTimeUnixNano - startTimeUnixNano)/1000000.0,
p99는 approx_percentile_cont(x, 0.99), 클라우드 구분은 resource['attributes']['cloud.provider']로 계산합니다.
“0건이면 데이터 없음” 규칙을 추가한 뒤에는, 애플리케이션 로그가 수집되지 않은 상황을 에이전트가 “데이터 없음”으로 정확히 보고했습니다. 반면 인과관계를 잘못 판단하는 문제는 프롬프트만으로 막기 어렵습니다. 그래서 에이전트의 결론은 가설로 취급하고, 운영자가 근거 목록의 쿼리를 직접 다시 실행해 확인하도록 했습니다. 에이전트가 실행한 SQL과 도구 호출이 모두 트레이스로 남아 있어서 이런 검증이 가능합니다.
SQL을 직접 작성하지 않아도 웹 UI의 Traces 탭에서 같은 근거를 확인할 수 있습니다. 아래 화면에서는 전체 트레이스 47건 중 9건(19.1%)이 에러였고, 평균 212ms, P90 1.03초로 집계되었습니다. 트레이스 ID를 누르면 POST /orders와 inventory-db SELECT span의 구간별 소요 시간을 바로 볼 수 있습니다. 에이전트가 “DB 구간이 느리다”, “504 에러가 있다”고 보고했을 때 운영자가 가장 빠르게 확인할 수 있는 화면입니다.


이 단계를 마치면 에이전트의 진단 품질까지 Omni 데이터로 추적하고 개선할 수 있습니다.
운영 관점 체크포인트
실제 환경에 적용할 때 레이어별로 확인할 항목은 다음과 같습니다.
| 레이어 | 내용 |
|---|---|
| Azure 인증 | 정적 AWS 키 없이 OIDC로만 Role 수임, sub 조건으로 특정 VM만 허용 |
| 네트워크 | VM 인바운드 규칙 없음, OTLP는 127.0.0.1에서만 수신 |
| 에이전트 권한 | 도구는 모두 읽기 전용, SQL은 SELECT 계열 단일 문장만 허용 |
| 프롬프트 데이터 | span에 프롬프트와 응답 본문을 기록하지 않음 |
| Dataset Integration Role | logs.amazonaws.com만 수임 가능, aws:SourceArn으로 범위 제한 |
| 비용 | 로그와 트레이스 쿼리는 스캔한 GB 기준으로 과금 |
비용도 짚어보겠습니다. 공식 요금 페이지 기준으로 us-east-1의 로그와 트레이스 쿼리는 스캔한 GB당 $0.005이며, 월간 로그와 span 수집량의 5배까지는 무료로 스캔할 수 있습니다. 이번 실습의 쿼리는 한 번에 약 0.2MB~0.4MB를 스캔했습니다. 에이전트가 쿼리를 반복 실행하는 구조라면, 조회 시간 범위를 짧게 강제하는 것이 가장 효과적인 비용 관리 방법입니다. 또한 무료 체험으로 30일 동안 OpenTelemetry 수집에 쓸 수 있는 $1,000 크레딧이 제공됩니다(AWS Organization당 최대 10개 계정).
결론
이번 사례에서 확인한 핵심은, Azure VM의 에러를 추적하려고 Azure Portal과 CloudWatch 콘솔을 오가던 작업이 SQL 쿼리 하나로 해결된다는 점입니다. cloud.provider 속성 하나만으로 두 클라우드의 서비스가 같은 테이블에서 조회되었습니다. 그 위에서 동작하는 Strands 에이전트의 도구 호출과 토큰 사용량도 같은 방식으로 확인할 수 있었습니다.
AWS를 주력으로 쓰면서 Azure VM이나 AKS에 일부 워크로드를 운영하는 팀, 그리고 AI 에이전트를 운영 업무에 도입하려는 팀에 특히 잘 맞습니다. 도입 전에는 세 가지를 확인하시기 바랍니다. Dataset Integration과 OIDC Role을 만들 IAM 권한이 있는지, 에이전트가 쿼리를 반복할 때 스캔 비용을 어떻게 제한할지, 에이전트의 결론을 사람이 검증하는 절차를 둘지입니다.
작은 VM 한 대와 Dataset Integration만 있으면 멀티클라우드 관측을 바로 경험해볼 수 있습니다. 지금 운영 중인 에이전트에 omni_sql 도구 하나를 추가하는 것부터 시작해보시기 바랍니다.
- 발표: https://aws.amazon.com/ko/about-aws/whats-new/2026/09/amazon-cloudwatch-omni/
- 제품 페이지: https://aws.amazon.com/cloudwatch/omni/
- 문서: https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/cloudwatch-omni.html
- AI 에이전트 텔레메트리 전송: https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/omni-send-ai-agent-telemetry.html
- Azure에 CloudWatch Agent 설치: https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/install-CloudWatch-Agent-on-Azure.html
- SQL 쿼리 레퍼런스: https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/omni-sql-query-reference.html
- 요금: https://aws.amazon.com/cloudwatch/omni/pricing/
글 | 메가존클라우드 Commercial Managed Unit 최영훈 매니저


