Why this is the most important step: We never use root keys. We never hardcode credentials. First, create a secure, read-only IAM Role for your Lambda function.
Build the 'AWS Guardian': A Serverless AI Bot to Scan Your Account for Security & Cost Risks
Build the “AWS Guardian”: A Serverless AI Bot to Scan Your Account for Security & Cost Risks
Introduction: The Problem That Could Cost You Millions
A single misconfigured S3 bucket can cost a company millions. An open security group can expose your entire infrastructure to attackers. A forgotten Elastic IP or idle EC2 instance can drain thousands of dollars from your AWS bill every month.
AWS is powerful, but complex. Most engineers don’t realize they’ve left the door wide open until it’s too late.
What if you had an AI assistant to watch your back?
This project shows you how to build a “Guardian Bot”—a serverless AI chatbot that uses Amazon Bedrock to give you plain-English answers about your account’s security posture and cost optimization opportunities.
Ask it: “Are any of my S3 buckets public?” or “Show me my 5 most expensive EC2 instances.” It will scan your account in real-time and give you actionable insights.
This is the ultimate portfolio project to prove you’re ready for a real cloud job. Not a toy. Not a tutorial for the sake of a tutorial. This is a production-grade tool that solves a real-world problem.
Features: What Your AWS Guardian Can Do
Real-Time Security Analysis
Ask natural language questions about your account's security posture.
Cost-Saving Insights
Identify waste in real-time and get cost optimization opportunities.
Serverless & Scalable
Built on Lambda and API Gateway, this bot costs almost nothing to run.
Context-Aware Conversations
Uses DynamoDB to remember your conversation history for follow-up questions.
Architecture: How It Works
Here’s the high-level flow of how your AWS Guardian operates:
-
User sends a question (e.g., “Are my S3 buckets secure?”) via the API Gateway.
-
API Gateway triggers the main AWS Lambda Function.
-
The Lambda function retrieves chat history from DynamoDB to understand context.
-
It sends the question and conversation history to Amazon Bedrock (Claude or Llama 2).
-
Bedrock’s AI model understands the user’s intent (e.g., “User wants to check S3 bucket policies”).
-
The Lambda function (using a secure IAM Role) runs a specific AWS SDK command (e.g.,
list_buckets,get_bucket_policy). -
The Lambda function sends the raw technical data (e.g., a JSON policy document) back to Bedrock.
-
Bedrock translates the technical JSON into a human-readable answer (e.g., “Yes, your bucket ‘my-private-data’ is public. You should fix this immediately.”).
-
Lambda stores the response in DynamoDB and sends the answer back to the user.
Technologies:
- AI: Amazon Bedrock (Claude 3.5 Sonnet or Llama 2)
- Backend: AWS Lambda (Python or Node.js)
- API: Amazon API Gateway (HTTP API)
- Memory: Amazon DynamoDB (for chat session history)
- Security: AWS IAM Roles (for secure, read-only access to your account)
Query Flow: How a Question Becomes an Answer
The diagram above shows the two-phase process that happens when you ask a question:
Phase 1 - Intent Analysis (Left): Your question flows through API Gateway to Lambda, which retrieves your chat history from DynamoDB and sends everything to Bedrock AI. The AI determines what you’re asking for (e.g., “check S3 bucket public access”).
Phase 2 - Execution (Right): Lambda executes the appropriate AWS SDK calls (like list_buckets()), gets raw JSON data from AWS services, sends it back to Bedrock for translation into plain English, saves the conversation to DynamoDB, and returns a human-readable answer.
Total time: ~2.3 seconds from question to answer.
Detailed Step-by-Step Breakdown
Want even more detail? Here’s the complete 8-step process with exact timing for each operation:
This detailed view shows every single step with performance metrics. Notice how the Lambda function orchestrates everything - managing chat history, coordinating with Bedrock for AI analysis, calling AWS APIs for real data, and translating technical responses into actionable insights.
Step-by-Step Deployment Guide
Step 1 - The Foundation (IAM & Security)
What permissions does it need?
Your Lambda function needs two types of permissions:
-
AWS Account Read Access (to scan your resources):
SecurityAudit(AWS managed policy for read-only security checks)ViewOnlyAccess(AWS managed policy for read-only resource access)
-
Bedrock Model Access (to call the AI):
bedrock:InvokeModel(to send prompts to Claude/Llama)
# Create a trust policy file for Lambda
cat > trust-policy.json <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "lambda.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}
EOF # Create a custom policy for Bedrock access
cat > bedrock-policy.json <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"bedrock:InvokeModel"
],
"Resource": "*"
}
]
}
EOF # Create the role
aws iam create-role \
--role-name AWSGuardianBotRole \
--assume-role-policy-document file://trust-policy.json
# Attach the required policies
aws iam attach-role-policy \
--role-name AWSGuardianBotRole \
--policy-arn arn:aws:iam::aws:policy/SecurityAudit
aws iam attach-role-policy \
--role-name AWSGuardianBotRole \
--policy-arn arn:aws:iam::aws:policy/job-function/ViewOnlyAccess
aws iam attach-role-policy \
--role-name AWSGuardianBotRole \
--policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
aws iam put-role-policy \
--role-name AWSGuardianBotRole \
--policy-name BedrockAccess \
--policy-document file://bedrock-policy.json Step 2 - The Brain (Amazon Bedrock)
Enable model access in the Bedrock console:
- Go to the AWS Bedrock Console (us-east-1 region recommended for model availability).
- Navigate to Model Access in the left sidebar.
- Click Modify model access.
- Enable access to Claude 3.5 Sonnet or Llama 2.
- Submit the request (it usually takes 1-2 minutes to approve).
Important: Not all regions support all models. Use us-east-1 for the widest model selection.
Step 3 - The “Muscle” (AWS Lambda Function)
Create the Lambda function that will orchestrate everything.
Python Implementation:
import json
import boto3
import os
from datetime import datetime
# Initialize AWS clients
bedrock = boto3.client('bedrock-runtime', region_name='us-east-1')
dynamodb = boto3.resource('dynamodb')
ec2 = boto3.client('ec2')
s3 = boto3.client('s3')
iam = boto3.client('iam')
# DynamoDB table name (we'll create this in Step 4)
TABLE_NAME = os.environ.get('DYNAMODB_TABLE', 'AWSGuardianSessions')
table = dynamodb.Table(TABLE_NAME)
# Bedrock model ID
MODEL_ID = "anthropic.claude-3-5-sonnet-20241022-v2:0"
def lambda_handler(event, context):
"""Main Lambda handler"""
try:
# Parse the incoming request
body = json.loads(event.get('body', '{}'))
user_message = body.get('message', '')
session_id = body.get('session_id', 'default-session')
if not user_message:
return {
'statusCode': 400,
'body': json.dumps({'error': 'Message is required'})
}
# Get conversation history from DynamoDB
history = get_conversation_history(session_id)
# Analyze the user's intent using Bedrock
intent = analyze_intent(user_message, history)
# Execute the appropriate AWS SDK command based on intent
aws_data = execute_aws_check(intent)
# Generate a human-readable response using Bedrock
response = generate_response(user_message, aws_data, history)
# Save the conversation to DynamoDB
save_conversation(session_id, user_message, response)
return {
'statusCode': 200,
'headers': {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
},
'body': json.dumps({
'response': response,
'session_id': session_id
})
}
except Exception as e:
print(f"Error: {str(e)}")
return {
'statusCode': 500,
'body': json.dumps({'error': str(e)})
}
def get_conversation_history(session_id):
"""Retrieve conversation history from DynamoDB"""
try:
response = table.get_item(Key={'session_id': session_id})
return response.get('Item', {}).get('messages', [])
except Exception as e:
print(f"Error getting history: {str(e)}")
return []
def analyze_intent(message, history):
"""Use Bedrock to understand what the user is asking for"""
system_prompt = """You are an AWS Security and Cost Analysis expert.
Your job is to understand what AWS resource the user wants to check.
Respond with a JSON object containing:
- "service": The AWS service (s3, ec2, iam, security_groups, etc.)
- "action": The specific check (list_buckets, check_public_access, list_instances, etc.)
- "context": Any specific filters or requirements
Example:
User: "Are any S3 buckets public?"
Response: {"service": "s3", "action": "check_public_access", "context": "all"}
"""
prompt = f"{system_prompt}\n\nUser question: {message}\n\nRespond with JSON only:"
response = bedrock.invoke_model(
modelId=MODEL_ID,
body=json.dumps({
"anthropic_version": "bedrock-2023-05-31",
"max_tokens": 500,
"messages": [
{"role": "user", "content": prompt}
]
})
)
result = json.loads(response['body'].read())
content = result['content'][0]['text']
# Extract JSON from the response
try:
return json.loads(content)
except:
# Fallback if Bedrock doesn't return valid JSON
return {"service": "unknown", "action": "help", "context": ""}
def execute_aws_check(intent):
"""Execute the appropriate AWS SDK command based on intent"""
service = intent.get('service', 'unknown')
action = intent.get('action', 'help')
try:
if service == 's3' and action == 'check_public_access':
return check_s3_public_buckets()
elif service == 'ec2' and action == 'list_instances':
return list_ec2_instances()
elif service == 'ec2' and action == 'check_costs':
return analyze_ec2_costs()
elif service == 'security_groups' and action == 'check_open':
return check_open_security_groups()
elif service == 'iam' and action == 'check_mfa':
return check_iam_mfa_status()
else:
return {"info": "I can help you check S3 buckets, EC2 instances, security groups, and IAM settings. What would you like to know?"}
except Exception as e:
return {"error": str(e)}
def check_s3_public_buckets():
"""Check for publicly accessible S3 buckets"""
try:
buckets = s3.list_buckets()['Buckets']
public_buckets = []
for bucket in buckets:
bucket_name = bucket['Name']
try:
# Check bucket ACL
acl = s3.get_bucket_acl(Bucket=bucket_name)
for grant in acl['Grants']:
grantee = grant.get('Grantee', {})
if grantee.get('Type') == 'Group' and 'AllUsers' in grantee.get('URI', ''):
public_buckets.append(bucket_name)
break
except Exception as e:
print(f"Error checking bucket {bucket_name}: {str(e)}")
return {
"total_buckets": len(buckets),
"public_buckets": public_buckets,
"bucket_names": [b['Name'] for b in buckets]
}
except Exception as e:
return {"error": str(e)}
def list_ec2_instances():
"""List all EC2 instances with their status"""
try:
instances = ec2.describe_instances()
instance_list = []
for reservation in instances['Reservations']:
for instance in reservation['Instances']:
instance_list.append({
'id': instance['InstanceId'],
'type': instance['InstanceType'],
'state': instance['State']['Name'],
'launch_time': str(instance['LaunchTime'])
})
return {
"total_instances": len(instance_list),
"instances": instance_list
}
except Exception as e:
return {"error": str(e)}
def check_open_security_groups():
"""Check for security groups with 0.0.0.0/0 access"""
try:
security_groups = ec2.describe_security_groups()['SecurityGroups']
open_groups = []
for sg in security_groups:
for rule in sg.get('IpPermissions', []):
for ip_range in rule.get('IpRanges', []):
if ip_range.get('CidrIp') == '0.0.0.0/0':
open_groups.append({
'group_id': sg['GroupId'],
'group_name': sg['GroupName'],
'port': rule.get('FromPort', 'All'),
'protocol': rule.get('IpProtocol', 'All')
})
break
return {
"total_security_groups": len(security_groups),
"open_to_internet": open_groups
}
except Exception as e:
return {"error": str(e)}
def check_iam_mfa_status():
"""Check IAM users without MFA enabled"""
try:
users = iam.list_users()['Users']
users_without_mfa = []
for user in users:
username = user['UserName']
mfa_devices = iam.list_mfa_devices(UserName=username)['MFADevices']
if len(mfa_devices) == 0:
users_without_mfa.append(username)
return {
"total_users": len(users),
"users_without_mfa": users_without_mfa
}
except Exception as e:
return {"error": str(e)}
def analyze_ec2_costs():
"""Analyze EC2 instances by cost (simplified version)"""
try:
instances = ec2.describe_instances()
running_instances = []
# Simplified cost estimation (real implementation would use Cost Explorer API)
cost_map = {
't2.micro': 0.0116,
't2.small': 0.023,
't2.medium': 0.0464,
't3.micro': 0.0104,
't3.small': 0.0208,
't3.medium': 0.0416,
'm5.large': 0.096,
'm5.xlarge': 0.192,
}
for reservation in instances['Reservations']:
for instance in reservation['Instances']:
if instance['State']['Name'] == 'running':
instance_type = instance['InstanceType']
hourly_cost = cost_map.get(instance_type, 0.10) # Default estimate
monthly_cost = hourly_cost * 730 # Hours in a month
running_instances.append({
'id': instance['InstanceId'],
'type': instance_type,
'estimated_monthly_cost': round(monthly_cost, 2)
})
# Sort by cost
running_instances.sort(key=lambda x: x['estimated_monthly_cost'], reverse=True)
return {
"running_instances": len(running_instances),
"top_5_expensive": running_instances[:5],
"total_estimated_monthly_cost": round(sum(i['estimated_monthly_cost'] for i in running_instances), 2)
}
except Exception as e:
return {"error": str(e)}
def generate_response(user_message, aws_data, history):
"""Generate a human-readable response using Bedrock"""
system_prompt = """You are an AWS Security and Cost Expert assistant.
Your job is to translate technical AWS data into clear, actionable advice.
Rules:
1. Be direct and concise
2. If there are security issues, warn the user clearly
3. If there are cost savings opportunities, highlight them
4. Always cite the specific data you're referring to
5. Use simple language, not AWS jargon
"""
prompt = f"""User asked: {user_message}
Here is the data from AWS:
{json.dumps(aws_data, indent=2)}
Provide a clear, helpful response based on this data."""
response = bedrock.invoke_model(
modelId=MODEL_ID,
body=json.dumps({
"anthropic_version": "bedrock-2023-05-31",
"max_tokens": 1000,
"system": system_prompt,
"messages": [
{"role": "user", "content": prompt}
]
})
)
result = json.loads(response['body'].read())
return result['content'][0]['text']
def save_conversation(session_id, user_message, bot_response):
"""Save conversation to DynamoDB"""
try:
# Get existing messages
response = table.get_item(Key={'session_id': session_id})
messages = response.get('Item', {}).get('messages', [])
# Add new messages
messages.append({
'role': 'user',
'content': user_message,
'timestamp': datetime.utcnow().isoformat()
})
messages.append({
'role': 'assistant',
'content': bot_response,
'timestamp': datetime.utcnow().isoformat()
})
# Keep only last 20 messages (10 exchanges)
messages = messages[-20:]
# Save to DynamoDB
table.put_item(
Item={
'session_id': session_id,
'messages': messages,
'updated_at': datetime.utcnow().isoformat()
}
)
except Exception as e:
print(f"Error saving conversation: {str(e)}") Deploy the Lambda function:
# Zip the code
zip -j lambda.zip lambda_function.py
# Create the Lambda function
aws lambda create-function \
--function-name AWSGuardianBot \
--runtime python3.11 \
--role arn:aws:iam::YOUR_ACCOUNT_ID:role/AWSGuardianBotRole \
--handler lambda_function.lambda_handler \
--zip-file fileb://lambda.zip \
--timeout 30 \
--memory-size 512 \
--environment Variables={DYNAMODB_TABLE=AWSGuardianSessions} Stop Reading Tutorials and Watching Videos. Start Shipping.
You’ve seen how the AWS Guardian works. Now it’s your turn to build it. Sign up at DeployU and start proving your skills with production-grade cloud solutions that actually deploy—not just sandbox demos.
Step 4 - The Memory (DynamoDB)
Create a DynamoDB table to store conversation history:
aws dynamodb create-table \
--table-name AWSGuardianSessions \
--attribute-definitions \
AttributeName=session_id,AttributeType=S \
--key-schema \
AttributeName=session_id,KeyType=HASH \
--billing-mode PAY_PER_REQUEST Why DynamoDB?
- Serverless (no servers to manage)
- Scales automatically
- Pay only for what you use
- Perfect for session data
Step 5 - The Front Door (API Gateway)
Create an HTTP API to expose your chatbot:
# Create the API
aws apigatewayv2 create-api \
--name AWSGuardianAPI \
--protocol-type HTTP \
--target arn:aws:lambda:us-east-1:YOUR_ACCOUNT_ID:function:AWSGuardianBot
# Get the API endpoint (it will be in the output)
aws apigatewayv2 get-apis aws lambda add-permission \
--function-name AWSGuardianBot \
--statement-id apigateway-invoke \
--action lambda:InvokeFunction \
--principal apigateway.amazonaws.com Your API endpoint will look like:
https://abc123xyz.execute-api.us-east-1.amazonaws.com
How to Use Your Guardian Bot
Test with curl:
curl -X POST https://YOUR_API_ID.execute-api.us-east-1.amazonaws.com \
-H "Content-Type: application/json" \
-d '{
"message": "Are any of my S3 buckets public?",
"session_id": "user-123"
}' Expected Response:
{
"response": "I checked all 12 of your S3 buckets. Good news: none of them are publicly accessible. All buckets have proper ACL restrictions in place.",
"session_id": "user-123"
} Try these questions:
- “Are any security groups open to 0.0.0.0/0?”
- “Show me my 5 most expensive EC2 instances”
- “Which IAM users don’t have MFA enabled?”
- “List all my running EC2 instances”
Why This Breaks (And How to Fix It)
Pitfall #1: AccessDeniedException from Lambda
The Error:
An error occurred (AccessDeniedException) when calling the ListBuckets operation:
User: arn:aws:sts::123456789:assumed-role/AWSGuardianBotRole/AWSGuardianBot
is not authorized to perform: s3:ListBucketsWhy it happens: Your Lambda function’s IAM Role is missing the required permissions. You can’t just “ask” about S3 buckets—you must explicitly grant the role s3:ListBuckets and s3:GetBucketAcl permissions.
The Fix:
Add this custom policy to your AWSGuardianBotRole:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:ListAllMyBuckets",
"s3:GetBucketAcl",
"s3:GetBucketPolicy",
"ec2:DescribeInstances",
"ec2:DescribeSecurityGroups",
"iam:ListUsers",
"iam:ListMFADevices"
],
"Resource": "*"
}
]
}aws iam put-role-policy \
--role-name AWSGuardianBotRole \
--policy-name AWSReadAccess \
--policy-document file://read-access-policy.json Pitfall #2: The Bot Gives Incorrect or Generic Answers
The Problem: Bedrock is “hallucinating” or giving vague responses like “I don’t have enough information.”
Why it happens: This is a prompt engineering problem. Your prompt to Bedrock needs to be very specific and structured.
The Fix:
Improve your system prompt to be more directive:
system_prompt = """You are an AWS Security and Cost Expert. You MUST use the provided AWS data to answer questions.
RULES:
1. ONLY use the data provided in the "AWS Data" section
2. If the data shows security issues, you MUST warn the user
3. If the data shows no issues, you MUST say "no issues found"
4. NEVER say "I don't have access" - the data is already provided
5. Be specific: cite bucket names, instance IDs, and numbers
6. If there's an error in the data, explain it clearly
FORMAT:
- Start with a direct answer (Yes/No/Found X issues)
- Then provide details from the data
- End with a recommendation if needed
"""Example of a good vs bad prompt:
❌ Bad: “Tell me about the S3 buckets”
✅ Good: “Based on this data: {...}, are any S3 buckets public? List the bucket names if yes.”
Pitfall #3: High Bedrock Costs
The Problem: Your Bedrock bill is unexpectedly high.
Why it happens: You’re sending too many tokens (long conversation history, verbose prompts).
The Fix:
-
Limit conversation history:
# Keep only last 10 messages (5 exchanges) messages = messages[-10:] -
Use a smaller model for intent analysis:
# Use Haiku for intent, Sonnet only for final response INTENT_MODEL = "anthropic.claude-3-haiku-20240307-v1:0" RESPONSE_MODEL = "anthropic.claude-3-5-sonnet-20241022-v2:0" -
Set max_tokens appropriately:
# Intent analysis: 200 tokens max # Final response: 500 tokens max (not 4000)
Future Enhancements & Next Steps
Once you have the basic Guardian Bot working, here are powerful extensions:
1. Add More Security Checks:
- Check for unused IAM access keys
- Scan for unencrypted RDS databases
- Find Lambda functions with overly permissive roles
2. Integrate with Slack or Microsoft Teams:
- Send daily security summaries to a Slack channel
- Allow team members to query the bot from Teams
3. Add Cost Forecasting:
- Integrate with AWS Cost Explorer API
- Predict next month’s bill based on current usage
4. Automated Remediation:
- “Fix it” button to automatically close open security groups
- One-click to delete unused Elastic IPs
5. Schedule Regular Scans:
- Use EventBridge to run weekly security audits
- Email reports to your team
Contributing:
This is an open-source learning project. If you add new checks or fix bugs, please contribute back to the community. Create a pull request with:
- Clear description of what you added
- Example input/output
- Any new IAM permissions required
Your Cloud Career Starts With What You Deploy
You’ve learned how to build an AWS Guardian bot. Now imagine having 10+ production-ready projects in your portfolio. Start building at DeployU and ship solutions that prove you’re job-ready—not just tutorial-ready.
Conclusion: More Than a Chatbot—A Career Accelerator
This project is more than a chatbot. It’s a real-world automation tool that solves actual problems companies face every day.
What you’ve learned:
- How to architect serverless AI applications on AWS
- How to use Amazon Bedrock for natural language understanding
- How to secure Lambda functions with IAM roles (no hardcoded keys)
- How to integrate multiple AWS services (Lambda, DynamoDB, API Gateway, Bedrock)
- How to build conversational interfaces with memory
- How to translate technical data into human-readable insights
Why this gets you hired:
When you walk into an interview and say, “I built an AI bot that scans AWS accounts for security risks and cost waste,” you’re speaking the language of real engineering problems.
You’re not just another candidate who did LeetCode. You’re someone who can ship production code to AWS.
Add this to your portfolio. Deploy it. Show it to recruiters. This is the 1% skill that separates hired engineers from the rest.
Stop Reading. Start Deploying.
This was a complex, multi-service build. If you want to deploy this entire project in under 15 minutes, on a real, risk-free AWS sandbox with a pre-configured environment, check out the “AWS Guardian Bot” lab on DeployU.
Our flat-fee plan means you get to build, break, and fix real systems like this with zero risk of a surprise AWS bill. Ever.
What you get with DeployU:
- ✅ Pre-configured AWS environment with Bedrock access
- ✅ Step-by-step guided labs with instant validation
- ✅ Real AWS credentials (not simulations)
- ✅ Hard spending limits (₹500/month max)
- ✅ Automated error detection with fix suggestions
- ✅ Portfolio-ready projects with shareable URLs
- ✅ Blockchain-verified certificates for LinkedIn
89% callback rate for students with 4+ deployed DeployU projects—compared to 23% for those with only LeetCode profiles.
Traditional cloud learning costs hundreds in AWS bills. DeployU provides the same real infrastructure for ₹999/month, with institutional plans at ₹15,000 per student for 3 years.
Stop reading tutorials and watching videos. Start shipping real solutions. Sign up at DeployU → and build the portfolio that gets you hired.
Join 5,000+ students who chose deployments over theory and landed offers at Amazon, Microsoft, and Google Cloud.
