AWS Builder Center
Your AI Agent Shouldn't Have the Keys to Production: 3 Guardrails Every AWS Agent Needs

Your AI Agent Shouldn't Have the Keys to Production: 3 Guardrails Every AWS Agent Needs

Everyone is wiring AI agents to AWS right now, and almost every tutorial skips the scary part: what happens when the agent is wrong?

This guide walks through a small Cloud Cost Detective built with Strands Agents + MCP + Amazon Bedrock. It finds idle EC2 instances and proposes fixes. Then it covers the three guardrails that make an agent like this safe to run against a real account:
  1. Layer 1: Least privilege. The agent's IAM role physically cannot delete things.
  2. Layer 2: Human-in-the-loop. Anything that changes state needs explicit human approval.
  3. Layer 3: Budget and loop limits. The agent can't run, or spend, forever.

The architecture

1
2
3
4
5
You ──► Strands Agent (Bedrock model)
│
├── MCP server: AWS Documentation (read-only knowledge)
├── Tool: find_idle_instances() (EC2 + CloudWatch, read-only)
└── Tool: stop_instance() (write, approval required)
Prerequisites: Python 3.10+, uv, AWS credentials, and Amazon Bedrock model access.
bash
1
2
pip install strands-agents boto3 mcp
export MODEL_ID=<a Bedrock model ID you have access to>
Keep the model ID in an environment variable. Model IDs and availability change often.

Layer 1: Least privilege

Prompts are not a security boundary. IAM is. Give the agent a dedicated role with only what it needs:
json
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ReadOnlyInsights",
"Effect": "Allow",
"Action": ["ec2:DescribeInstances", "cloudwatch:GetMetricStatistics"],
"Resource": "*"
},
{
"Sid": "StopOnlyTaggedInstances",
"Effect": "Allow",
"Action": "ec2:StopInstances",
"Resource": "arn:aws:ec2:*:*:instance/*",
"Condition": {
"StringEquals": { "aws:ResourceTag/agent-managed": "true" }
}
},
{
"Sid": "InvokeModel",
"Effect": "Allow",
"Action": ["bedrock:InvokeModel", "bedrock:InvokeModelWithResponseStream"],
"Resource": "*"
}
]
}
Notice what's missing: no TerminateInstances, no Delete*, and StopInstances only works on instances tagged agent-managed=true. Even a fully jailbroken agent can't touch an untagged production server.

Layer 2: A read tool and a gated write tool

In Strands, a tool is just a decorated Python function.
python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import os
from datetime import datetime, timedelta, timezone
import boto3
from strands import Agent, tool
from strands.models import BedrockModel

ec2 = boto3.client("ec2")
cw = boto3.client("cloudwatch")

@tool
def find_idle_instances(cpu_threshold: float = 5.0, days: int = 7) -> list:
"""Find running EC2 instances with average CPU below the threshold."""
end = datetime.now(timezone.utc)
start = end - timedelta(days=days)
idle = []
for r in ec2.describe_instances(
Filters=[{"Name": "instance-state-name", "Values": ["running"]}]
)["Reservations"]:
for i in r["Instances"]:
points = cw.get_metric_statistics(
Namespace="AWS/EC2", MetricName="CPUUtilization",
Dimensions=[{"Name": "InstanceId", "Value": i["InstanceId"]}],
StartTime=start, EndTime=end, Period=86400, Statistics=["Average"],
)["Datapoints"]
if points:
avg = sum(p["Average"] for p in points) / len(points)
if avg < cpu_threshold:
idle.append({"id": i["InstanceId"], "avg_cpu": round(avg, 2)})
return idle

@tool
def stop_instance(instance_id: str) -> str:
"""Stop an EC2 instance. Requires human approval."""
if input(f"Agent wants to STOP {instance_id}. Approve? [y/N]: ").lower() != "y":
return "Denied by human. Instance NOT stopped."
ec2.stop_instances(InstanceIds=[instance_id])
return f"Stopped {instance_id}."
The key design choice: the approval check lives inside the tool code, not in the prompt. The model can't talk its way around a Python input() call.

Add MCP for live AWS knowledge

The Model Context Protocol lets an agent use external tool servers without custom glue. Here, the open-source AWS Documentation MCP server lets the agent look up best practices instead of guessing.
python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from mcp import StdioServerParameters, stdio_client
from strands.tools.mcp import MCPClient

docs_mcp = MCPClient(lambda: stdio_client(StdioServerParameters(
command="uvx", args=["awslabs.aws-documentation-mcp-server@latest"]
)))

model = BedrockModel(model_id=os.environ["MODEL_ID"], temperature=0.2, max_tokens=1024)

with docs_mcp:
agent = Agent(
model=model,
system_prompt="You are a careful AWS FinOps assistant. Verify with tools "
"before making claims. Never guess.",
tools=[find_idle_instances, stop_instance] + docs_mcp.list_tools_sync(),
)
agent("Do I have idle EC2 instances? Suggest what to stop.")

Layer 3: Limits on loops and spend

Agents can get stuck calling tools in circles. Two simple defenses:
  • Cap the output. max_tokens and a low temperature (already set above) keep each call bounded.
  • Alert on spend. Create an AWS Budgets alarm (for example, $10/month with an alert at 80%) on the account or tag the agent uses.
Also log every tool call so you can audit exactly what the agent did, and add a maximum tool-call count in your own wrapper.

Taking it to production

A laptop script is great for learning. For team use, look at Amazon Bedrock AgentCore, which offers managed runtime, gateway, memory and observability for agents. Check the latest docs for current features and regional availability.

Key takeaways

RiskGuardrailWhere it lives
Agent deletes somethingLeast-privilege IAM + tag conditionAWS IAM
Agent acts without consentApproval gate inside the toolYour code
Agent loops or overspendsToken cap + AWS Budgets alarmModel config + AWS
The pattern to remember: let the model reason, but let infrastructure enforce.
Any opinions in this article are those of the individual author and may not reflect the opinions of AWS.
Enjoyed reading this content? Let the author know!

Your likes, comments, shares, and saves help creators reach more builders.

Loading recommendations

Loading article