โ† AWS Observability & Incident Response

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.

Some setupโ‰ˆ 15-30 minaws

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.

VariableWhat it isWhere 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_ID
secretoptional
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_KEY
secretoptional
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_TOKEN
secretoptional
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 required

Single-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 required

Grants 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 required

Grants 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 required

Grants 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 policy

Starts 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 policy

Runs 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 policy

Lists 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 required

Runs 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 required

Checks 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 policy

Creates 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 policy

Publishes 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 policy

Sets 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 required

Lets 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. 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. 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. 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. 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. 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. 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. 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:StartQuery

Cause: 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 expired

Cause: 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 invalid

Cause: 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 operation

Cause: 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 operation

Cause: 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 exist

Cause: 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 operation

Cause: 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 operation

Cause: 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 operation

Cause: 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 exceeded

Cause: 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 operation

Cause: 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 operation

Cause: 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.

OperationCallPermissionRef
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 jsonsts:GetCallerIdentitydocs โ†—
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_REGIONhealth:DescribeEventsdocs โ†—
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:DescribeAlarmsdocs โ†—
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:DescribeAlarmHistorydocs โ†—
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 TimestampDescendingcloudwatch:GetMetricDatadocs โ†—
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 notBreachingcloudwatch:PutMetricAlarmdocs โ†—
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.jsoncloudwatch:PutDashboarddocs โ†—
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:DescribeLogGroupsdocs โ†—
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:StartQuerydocs โ†—
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:GetQueryResultsdocs โ†—
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:StopQuerydocs โ†—
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 ERRORlogs:StartLiveTaildocs โ†—
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 30logs:PutRetentionPolicydocs โ†—
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:GetServiceGraphdocs โ†—
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:GetTraceSummariesdocs โ†—
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:BatchGetTracesdocs โ†—
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 50cloudtrail:LookupEventsdocs โ†—
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:GetTrailStatusdocs โ†—
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:GetEventSelectorsdocs โ†—
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:StartQuerydocs โ†—