Setup guide
Everything you must provision before this skill can run โ the exact credentials, permissions, and where to click. Links go to the official consoles and docs, which stay current.
An AWS account already emitting CloudWatch metrics and logs with at least one active CloudTrail trail, plus a read-only principal (IAM Identity Center profile or role) the agent can assume in the incident's region.
You need to already have
- AWS CLI v2 installed and on PATH (`aws --version` reports 2.x); Python 3.9+ with `boto3>=1.34` for the scripted path
- An AWS account, plus someone with IAM admin rights who can create a role or an IAM Identity Center permission set
- At least one CloudTrail trail (or a CloudTrail Lake event data store) actively logging in the target region
- CloudWatch Logs groups already receiving application logs; X-Ray or ADOT instrumentation if trace analysis is wanted
- AWS Business or Enterprise Support if you want the AWS Health API (`health:DescribeEvents`); Basic/Developer returns SubscriptionRequiredException
How the pieces connect
Credentials this skill needs
Set these as environment variables. Never paste secrets into a chat or commit them.
| Variable | What it is | Where to get it |
|---|---|---|
AWS_PROFILE | Named AWS CLI / SSO profile format: profile name defined in ~/.aws/config, e.g. prod-observer | Run `aws configure sso --profile prod-observer` (IAM Identity Center start URL + region + permission set), then `aws sso login --profile prod-observer`. List existing profiles with `aws configure list-profiles`. |
AWS_REGION | Target region for the investigation format: region code, e.g. us-east-1 or eu-west-1 | Read the current default with `aws configure get region`, or take it from the region selector in the top-right of the AWS Management Console. CloudWatch, Logs and X-Ray are regional; AWS Health and IAM are global via us-east-1. |
AWS_ACCESS_KEY_IDsecretoptional | Access key id (only if not using SSO) format: 20-character string starting with AKIA (long-lived IAM user) or ASIA (temporary STS session) | IAM console > Users > <user> > Security credentials > Create access key, or from the output of `aws sts assume-role --role-arn <arn> --role-session-name obs`. Prefer IAM Identity Center over long-lived AKIA keys. |
AWS_SECRET_ACCESS_KEYsecretoptional | Secret access key (only if not using SSO) format: 40-character base64-like string | Shown exactly once when the access key is created (IAM console or `aws iam create-access-key`). If lost, run `aws iam delete-access-key` and create a new one. |
AWS_SESSION_TOKENsecretoptional | Session token (temporary credentials only) format: long opaque string, several hundred characters | Returned alongside ASIA keys by `aws sts assume-role` or `aws sso login`. Mandatory whenever AWS_ACCESS_KEY_ID starts with ASIA; omitting it yields InvalidClientTokenId. |
Permissions to grant
arn:aws:iam::aws:policy/ReadOnlyAccessIAM policyadmin consent requiredSingle-attachment grant that covers every read call this skill makes across CloudWatch, Logs, X-Ray, CloudTrail and the resource APIs used to resolve ARNs.
โณ Prefer the three service-scoped read policies below; ReadOnlyAccess also grants data reads such as s3:GetObject and dynamodb:GetItem that this skill never needs.
arn:aws:iam::aws:policy/CloudWatchReadOnlyAccessIAM policyadmin consent requiredGrants cloudwatch:GetMetricData, cloudwatch:DescribeAlarms, cloudwatch:DescribeAlarmHistory and the CloudWatch Logs read/query actions used in steps 2 and 3.
โณ Replace with an inline policy granting only cloudwatch:GetMetricData, cloudwatch:ListMetrics, cloudwatch:DescribeAlarms, cloudwatch:DescribeAlarmHistory, logs:DescribeLogGroups, logs:StartQuery, logs:GetQueryResults and logs:StopQuery.
arn:aws:iam::aws:policy/AWSXrayReadOnlyAccessIAM policyadmin consent requiredGrants xray:GetServiceGraph, xray:GetTraceSummaries and xray:BatchGetTraces for the trace localisation step.
โณ Skip entirely if the workload has no X-Ray or ADOT instrumentation.
arn:aws:iam::aws:policy/AWSCloudTrail_ReadOnlyAccessIAM policyadmin consent requiredGrants cloudtrail:LookupEvents, cloudtrail:DescribeTrails, cloudtrail:GetTrailStatus and cloudtrail:GetEventSelectors for the change-forensics step.
โณ An inline policy with just cloudtrail:LookupEvents and cloudtrail:GetTrailStatus covers the 90-day management-event path.
logs:StartQueryIAM policyStarts each CloudWatch Logs Insights query; paired with logs:GetQueryResults and logs:StopQuery for the async poll-and-release loop.
โณ Scope the Resource to the specific log groups, e.g. arn:aws:logs:us-east-1:123456789012:log-group:/aws/ecs/prod-api:*
cloudwatch:GetMetricDataIAM policyRuns the batched metric + metric-math queries (error rate, p99 latency, FILL) that quantify blast radius.
โณ GetMetricData does not support resource-level scoping; constrain with a cloudwatch:namespace condition key instead.
cloudtrail:LookupEventsIAM policyLists control-plane changes in the incident window to find the deploy, scaling action or security-group edit that preceded the metric inflection.
โณ Read-only and cannot be resource-scoped; pair with cloudtrail:GetTrailStatus so an empty result can be distinguished from a stopped trail.
cloudtrail:StartQueryIAM policyadmin consent requiredRuns SQL over a CloudTrail Lake event data store when the evidence is older than the 90-day LookupEvents window or needs aggregation.
โณ Scope Resource to the specific event data store ARN; omit entirely if CloudTrail Lake is not in use.
health:DescribeEventsIAM policyadmin consent requiredChecks for an open AWS-side service event before the agent spends time blaming the customer's own code.
โณ Requires a Business or Enterprise Support plan; drop it and skip step 1's Health check on Basic/Developer.
cloudwatch:PutMetricAlarmIAM policyCreates or repairs the alarm that should have caught this incident. Gated behind explicit user confirmation because alarms page humans.
โณ Scope Resource to arn:aws:cloudwatch:<region>:<account>:alarm:<prefix>* so the skill cannot overwrite unrelated alarms.
cloudwatch:PutDashboardIAM policyPublishes the incident dashboard that pins the metrics used during triage.
โณ Dashboards are account-global; scope by dashboard name prefix in the Resource element.
logs:PutRetentionPolicyIAM policySets retention on log groups found with retentionInDays: null, which is both a cost and a forensics gap.
โณ Scope to the specific log-group ARNs; this action can shorten retention and destroy evidence, so confirm before use.
sts:AssumeRoleIAM policyadmin consent requiredLets the agent's base principal assume the read-only observability role in the target (often cross-account) workload account.
โณ Scope to the single role ARN and require an ExternalId condition for cross-account use.
Step by step
- 1
Install and verify AWS CLI v2
Install from the official installer, then confirm the major version: `aws --version` must print aws-cli/2.x. CLI v1 lacks `aws logs tail` and `aws logs start-live-tail`. For the scripted path also run `python -m pip install 'boto3>=1.34'`.
Open in the console / docs โ - 2
Create a read-only observability role
`aws iam create-role --role-name ObservabilityReader --assume-role-policy-document file://trust.json` then attach the read policies: `aws iam attach-role-policy --role-name ObservabilityReader --policy-arn arn:aws:iam::aws:policy/CloudWatchReadOnlyAccess` (repeat for arn:aws:iam::aws:policy/AWSXrayReadOnlyAccess and arn:aws:iam::aws:policy/AWSCloudTrail_ReadOnlyAccess).
Open in the console / docs โ - 3
Authenticate with IAM Identity Center (preferred over static keys)
`aws configure sso --profile prod-observer` (supply the SSO start URL, SSO region, account and permission set), then `aws sso login --profile prod-observer`. Export `AWS_PROFILE=prod-observer` and `AWS_REGION=<incident region>` before running anything.
Open in the console / docs โ - 4
Confirm a CloudTrail trail exists and is actually logging
`aws cloudtrail describe-trails --query 'trailList[].[Name,IsMultiRegionTrail,HomeRegion]' --output table` then `aws cloudtrail get-trail-status --name <trail-arn> --query '[IsLogging,LatestDeliveryError]'`. If IsLogging is false, forensics in step 5 of the procedure will silently return nothing.
Open in the console / docs โ - 5
Inventory log groups and find missing retention
`aws logs describe-log-groups --query 'logGroups[?retentionInDays==null].logGroupName' --output table`. Record the group names the workload writes to; the agent needs them for every Logs Insights query.
Open in the console / docs โ - 6
Enable tracing if you want the X-Ray step to work
Turn on active tracing per service (Lambda `--tracing-config Mode=Active`, API Gateway stage tracing, ECS/EKS sidecar or ADOT collector). Verify with `aws xray get-service-graph --start-time $(date -u -d '-1 hour' +%s) --end-time $(date -u +%s)`; an empty Services array means tracing is off.
Open in the console / docs โ - 7
Smoke-test each data plane before an incident starts
Run one call per service: `aws sts get-caller-identity`, `aws cloudwatch describe-alarms --max-records 1`, `aws logs describe-log-groups --limit 1`, `aws cloudtrail lookup-events --max-results 1`, `aws xray get-service-graph --start-time $(date -u -d '-10 min' +%s) --end-time $(date -u +%s)`. Any AccessDenied here is a missing permission, not an incident.
Open in the console / docs โ
Check it worked
aws sts get-caller-identity --output table && aws cloudwatch describe-alarms --max-records 1 --query 'MetricAlarms[].AlarmName' --output text
If you hit an error
An error occurred (AccessDeniedException) when calling the StartQuery operation: User: arn:aws:sts::123456789012:assumed-role/ObservabilityReader/session is not authorized to perform: logs:StartQueryCause: The role can describe log groups but was never granted the Logs Insights query actions, or the policy Resource does not cover this log group.
Fix: Add logs:StartQuery, logs:GetQueryResults and logs:StopQuery with Resource arn:aws:logs:<region>:<account>:log-group:<name>:* (note the trailing :*).
An error occurred (ExpiredTokenException) when calling the GetMetricData operation: The security token included in the request is expiredCause: The IAM Identity Center session or assumed-role session reached its duration limit mid-investigation.
Fix: Re-run `aws sso login --profile <profile>` (or re-assume the role) and retry; do not fall back to long-lived AKIA keys.
An error occurred (InvalidClientTokenId) when calling the GetCallerIdentity operation: The security token included in the request is invalidCause: AWS_ACCESS_KEY_ID starts with ASIA but AWS_SESSION_TOKEN is missing, or the access key was deleted.
Fix: Export AWS_SESSION_TOKEN alongside the ASIA key, or create a fresh key; `unset AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN` before using AWS_PROFILE.
An error occurred (LimitExceededException) when calling the StartQuery operationCause: The account is at its cap of concurrent CloudWatch Logs Insights queries, usually from abandoned queries that were never stopped.
Fix: Run `aws logs describe-queries --status Running` and `aws logs stop-query --query-id <id>` for stale ids, then serialize queries instead of fanning out.
An error occurred (MalformedQueryException) when calling the StartQuery operationCause: Invalid Logs Insights syntax - a leading pipe, an unescaped slash inside a regex, or a field that does not exist in the group.
Fix: Check available fields with `aws logs get-log-group-fields --log-group-name <name>` and validate the query string before re-running.
An error occurred (ResourceNotFoundException) when calling the StartQuery operation: The specified log group does not existCause: Log-group name typo, or the CLI is pointed at a different region from the one the workload logs into.
Fix: Confirm with `aws logs describe-log-groups --log-group-name-prefix <prefix> --region <region>` and set AWS_REGION explicitly.
An error occurred (InvalidParameterCombination) when calling the GetMetricStatistics operationCause: The requested period is too fine for the age of the data - CloudWatch keeps 1-minute data for 15 days, 5-minute for 63 days, 1-hour for 455 days.
Fix: Increase --period to 300 or 3600, or narrow the start/end window; prefer get-metric-data, which surfaces the same limits more clearly.
An error occurred (InvalidLookupAttributesException) when calling the LookupEvents operationCause: More than one entry was passed to --lookup-attributes, or an unsupported AttributeKey was used.
Fix: Pass exactly one attribute (EventName, EventSource, ReadOnly, Username, ResourceType, ResourceName, EventId or AccessKeyId) and filter the rest client-side.
An error occurred (SubscriptionRequiredException) when calling the DescribeEvents operationCause: The AWS Health API is only available on Business, Enterprise On-Ramp and Enterprise Support plans.
Fix: Skip the Health check and rely on the public Service Health Dashboard, or upgrade the support plan.
An error occurred (ThrottlingException) when calling the GetTraceSummaries operation: Rate exceededCause: Too many X-Ray or CloudWatch API calls per second, common when paginating traces in a tight loop.
Fix: Set AWS_RETRY_MODE=adaptive (or boto3 Config(retries={'mode':'adaptive','max_attempts':10})) and add a short sleep between pages.
An error occurred (UnauthorizedOperation) when calling the RebootInstances operationCause: The --dry-run permission check failed: the principal lacks ec2:RebootInstances on that instance.
Fix: Grant the action or use a different mitigation. Remember that DryRunOperation - not a silent success - is the signal that the real call would have worked.
An error occurred (AccessDenied) when calling the AssumeRole operationCause: The target role's trust policy does not list the calling principal, or an ExternalId condition is unmet.
Fix: Update the role trust policy (`aws iam update-assume-role-policy`) and pass --external-id on the assume-role call.
Official documentation: https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/WhatIsCloudWatch.html โ
API operations this skill uses (20)
Every call the skill makes, linked to its official reference page.
| Operation | Call | Permission | Ref |
|---|---|---|---|
| Verify caller identity and account Guardrail: prints the account id, principal ARN and user id that every later command will run as. | CLI aws sts get-caller-identity --output json | sts:GetCallerIdentity | docs โ |
| Check for open AWS-side service events Rules out an AWS platform incident before spending time on the customer's own code. | CLI aws health describe-events --region us-east-1 --filter eventStatusCodes=open,upcoming regions=$AWS_REGION | health:DescribeEvents | docs โ |
| List alarms currently in ALARM Establishes which monitors are firing and what reason string CloudWatch attached to each. | CLI aws cloudwatch describe-alarms --state-value ALARM --query 'MetricAlarms[].[AlarmName,StateReason,StateUpdatedTimestamp]' | cloudwatch:DescribeAlarms | docs โ |
| Read an alarm's state transitions Pins the exact minute the alarm flipped, which anchors the incident timeline. | CLI aws cloudwatch describe-alarm-history --alarm-name <name> --history-item-type StateUpdate --start-date <iso> --end-date <iso> | cloudwatch:DescribeAlarmHistory | docs โ |
| Batched metric query with metric math Quantifies blast radius: 5xx rate as a percentage of requests, p99 latency, FILL() over gaps, in one call. | CLI aws cloudwatch get-metric-data --metric-data-queries file://mdq.json --start-time <iso> --end-time <iso> --scan-by TimestampDescending | cloudwatch:GetMetricData | docs โ |
| Create or repair a metric alarm Closes the detection gap after the incident. Destructive-adjacent (it can page humans), so it is gated behind explicit confirmation. | CLI aws cloudwatch put-metric-alarm --alarm-name prod-api-5xx --namespace AWS/ApplicationELB --metric-name HTTPCode_Target_5XX_Count --statistic Sum --period 60 --evaluation-periods 5 --datapoints-to-alarm 3 --threshold 25 --comparison-operator GreaterThanThreshold --treat-missing-data notBreaching | cloudwatch:PutMetricAlarm | docs โ |
| Publish an incident dashboard Pins the metrics used during triage so the next responder starts where this one finished. | CLI aws cloudwatch put-dashboard --dashboard-name prod-api --dashboard-body file://dashboard.json | cloudwatch:PutDashboard | docs โ |
| Discover log groups for the workload Finds the groups to query and flags any with retentionInDays null as a forensics and cost gap. | CLI aws logs describe-log-groups --log-group-name-prefix /aws/ --query 'logGroups[].[logGroupName,retentionInDays]' | logs:DescribeLogGroups | docs โ |
| Start a Logs Insights query Primary log-analysis entry point; asynchronous, returns a queryId that must then be polled. | CLI aws logs start-query --log-group-names /aws/ecs/prod-api --start-time <epoch> --end-time <epoch> --query-string 'fields @timestamp, @message | filter @message like /(?i)exception/ | limit 200' | logs:StartQuery | docs โ |
| Poll and read query results Retrieves rows once status reaches Complete; returns Running with empty results until then. | CLI aws logs get-query-results --query-id <queryId> --query 'results[*][?field!=`@ptr`].value' | logs:GetQueryResults | docs โ |
| Release an abandoned query slot Concurrent Logs Insights queries are capped per account; leaking slots causes LimitExceededException later. | CLI aws logs stop-query --query-id <queryId> | logs:StopQuery | docs โ |
| Stream logs live with server-side filtering Watches events arrive in real time while a mitigation is applied, without re-running batch queries. | CLI aws logs start-live-tail --log-group-identifiers arn:aws:logs:us-east-1:123456789012:log-group:/aws/ecs/prod-api --log-event-filter-pattern ERROR | logs:StartLiveTail | docs โ |
| Set log retention Bounds unlimited-retention groups. Can shorten retention and destroy evidence, so it requires confirmation. | CLI aws logs put-retention-policy --log-group-name /aws/ecs/prod-api --retention-in-days 30 | logs:PutRetentionPolicy | docs โ |
| Fetch the X-Ray service graph Shows which node in the dependency graph is producing faults; an empty Services array means tracing is not enabled. | CLI aws xray get-service-graph --start-time <iso> --end-time <iso> --query 'Services[].{n:Name,flt:SummaryStatistics.FaultStatistics.TotalCount}' | xray:GetServiceGraph | docs โ |
| Find faulting or slow traces Narrows millions of requests to the handful of trace ids worth opening. | CLI aws xray get-trace-summaries --start-time <iso> --end-time <iso> --filter-expression 'fault = true AND responsetime > 3' | xray:GetTraceSummaries | docs โ |
| Pull full trace segment documents Reveals which downstream call inside the failing service consumed the latency. | CLI aws xray batch-get-traces --trace-ids 1-6874a2b1-1a2b3c4d5e6f7a8b9c0d1e2f --query 'Traces[].Segments[].Document' | xray:BatchGetTraces | docs โ |
| List control-plane changes in the window Surfaces the deploy, scaling change or security-group edit that preceded the metric inflection. Exactly one lookup attribute per call, 90-day history. | CLI aws cloudtrail lookup-events --start-time <iso> --end-time <iso> --lookup-attributes AttributeKey=ReadOnly,AttributeValue=false --max-results 50 | cloudtrail:LookupEvents | docs โ |
| Confirm the trail is actually logging Distinguishes 'nothing changed' from 'the trail was stopped', which are very different conclusions. | CLI aws cloudtrail get-trail-status --name <trail-arn> --query '[IsLogging,LatestDeliveryTime,LatestDeliveryError]' | cloudtrail:GetTrailStatus | docs โ |
| Check whether data events are captured S3 object-level and Lambda invoke events are off by default; this proves whether their absence is meaningful. | CLI aws cloudtrail get-event-selectors --trail-name <trail-arn> | cloudtrail:GetEventSelectors | docs โ |
| Run SQL over CloudTrail Lake Handles evidence older than the 90-day LookupEvents window, and aggregations LookupEvents cannot express. | CLI aws cloudtrail start-query --query-statement "SELECT eventTime, eventName, errorCode FROM <event-data-store-id> WHERE errorCode IS NOT NULL ORDER BY eventTime DESC LIMIT 100" | cloudtrail:StartQuery | docs โ |