Important: Review each command before executing. Replace placeholder values (<...>) with your actual resource IDs. Set your account ID first:
export AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
1. Delete Unused CloudWatch Dashboards ($12/month savings)
CloudWatch dashboards cost $3/month each. Remove dashboards no longer actively used for monitoring.
aws cloudwatch list-dashboards --region us-east-1 \
--query "DashboardEntries[].DashboardName" --output table
aws cloudwatch delete-dashboards --region us-east-1 \
--dashboard-names "Dashboard1" "Dashboard2" "Dashboard3"
2. Stop Non-Production Instances When Not in Use ($9.50/month)
Large EC2 instances (t2.xlarge) cost $0.1856/hr. Stop them when not actively needed and schedule restarts only during working hours.
aws ec2 stop-instances --region us-east-1 \
--instance-ids <INSTANCE_ID>
aws ec2 start-instances --region us-east-1 \
--instance-ids <INSTANCE_ID>
aws scheduler create-schedule --name "stop-non-prod-instances" \
--schedule-expression "cron(0 22 * * ? *)" \
--flexible-time-window "Mode=OFF" \
--target '{
"Arn": "arn:aws:scheduler:::aws-sdk:ec2:stopInstances",
"Input": "{\"InstanceIds\":[\"<INSTANCE_ID>\"]}",
"RoleArn": "arn:aws:iam::${AWS_ACCOUNT_ID}:role/SchedulerEC2Role"
}' --region us-east-1
3. Release Unattached Elastic IPs ($3.65/month each)
Elastic IPs not associated with a running instance are charged $0.005/hour ($3.65/mo). Find and release unused ones.
aws ec2 describe-addresses --region us-east-1 \
--query "Addresses[?AssociationId==null].{IP:PublicIp,AllocId:AllocationId}" \
--output table
aws ec2 release-address --region us-east-1 \
--allocation-id <ALLOCATION_ID>
4. Set CloudWatch Log Retention Policies ($3/month savings)
Log groups without retention policies store data indefinitely at $0.03/GB/month. Set retention to 7-30 days for non-critical logs.
aws logs describe-log-groups --region us-east-1 \
--query "logGroups[?retentionInDays==null].{Name:logGroupName,Size:storedBytes}" \
--output table
# For each non-critical log group:
aws logs put-retention-policy --region us-east-1 \
--log-group-name "/aws/lambda/MyFirstLambdaFn" \
--retention-in-days 7
aws logs put-retention-policy --region us-east-1 \
--log-group-name "LogGroup-MyDemo" \
--retention-in-days 7
for lg in $(aws logs describe-log-groups --region us-east-1 \
--query "logGroups[?retentionInDays==null].logGroupName" \
--output text); do
echo "Setting 30-day retention on: $lg"
aws logs put-retention-policy --region us-east-1 \
--log-group-name "$lg" --retention-in-days 30
done
5. Delete Unattached EBS Volumes ($0.08/GB/month)
EBS volumes in "available" state (not attached) still incur charges. Delete them if not needed, or snapshot and delete.
aws ec2 describe-volumes --region us-east-1 \
--filters "Name=status,Values=available" \
--query "Volumes[].{ID:VolumeId,Size:Size,Type:VolumeType}" \
--output table
aws ec2 create-snapshot --region us-east-1 \
--volume-id <VOLUME_ID> \
--description "Backup before deletion"
aws ec2 delete-volume --region us-east-1 \
--volume-id <VOLUME_ID>
6. Add S3 Lifecycle Policies (Transition to Glacier)
Move infrequently accessed objects to S3 Glacier after 30 days and delete after 365 days to reduce storage costs by up to 80%.
aws s3api put-bucket-lifecycle-configuration \
--bucket <BUCKET_NAME> --region us-east-1 \
--lifecycle-configuration '{
"Rules": [{
"ID": "TransitionToGlacier",
"Status": "Enabled",
"Filter": {"Prefix": ""},
"Transitions": [
{"Days": 30, "StorageClass": "STANDARD_IA"},
{"Days": 90, "StorageClass": "GLACIER"}
],
"Expiration": {"Days": 365}
}]
}'
7. Enable EBS Encryption by Default (Security + Compliance)
While not a direct cost saving, enabling default encryption prevents compliance violations that trigger costly remediation later.
aws ec2 enable-ebs-encryption-by-default --region us-east-1
aws ec2 get-ebs-encryption-by-default --region us-east-1
8. Create S3 VPC Gateway Endpoint (Free, Reduces Data Transfer)
S3 Gateway Endpoints are free and route S3 traffic within AWS network, avoiding data transfer charges and improving latency.
ROUTE_TABLE_ID=$(aws ec2 describe-route-tables --region us-east-1 \
--filters "Name=vpc-id,Values=<VPC_ID>" \
--query "RouteTables[0].RouteTableId" --output text)
aws ec2 create-vpc-endpoint --region us-east-1 \
--vpc-id <VPC_ID> \
--service-name com.amazonaws.us-east-1.s3 \
--route-table-ids $ROUTE_TABLE_ID
9. Set Up AWS Budget Alerts (Prevent Surprise Bills)
Create budget alerts that notify you when spending exceeds thresholds, preventing unexpected cost overruns.
aws budgets create-budget --region us-east-1 \
--account-id $AWS_ACCOUNT_ID \
--budget '{
"BudgetName": "Monthly-Cost-Alert",
"BudgetLimit": {"Amount": "50", "Unit": "USD"},
"TimeUnit": "MONTHLY",
"BudgetType": "COST"
}' \
--notifications-with-subscribers '[{
"Notification": {
"NotificationType": "ACTUAL",
"ComparisonOperator": "GREATER_THAN",
"Threshold": 80
},
"Subscribers": [{
"SubscriptionType": "EMAIL",
"Address": "<YOUR_EMAIL>"
}]
}]'
10. Enable Cost Anomaly Detection (Automated Monitoring)
Automatically detect unusual cost patterns and get notified before small anomalies become large surprises.
aws ce create-anomaly-monitor --region us-east-1 \
--anomaly-monitor '{
"MonitorName": "AllServices-Monitor",
"MonitorType": "DIMENSIONAL",
"MonitorDimension": "SERVICE"
}'
aws ce create-anomaly-subscription --region us-east-1 \
--anomaly-subscription '{
"SubscriptionName": "DailyAlerts",
"Frequency": "DAILY",
"MonitorArnList": ["<MONITOR_ARN>"],
"Subscribers": [{"Type": "EMAIL", "Address": "<YOUR_EMAIL>"}],
"ThresholdExpression": {
"Dimensions": {
"Key": "ANOMALY_TOTAL_IMPACT_ABSOLUTE",
"MatchOptions": ["GREATER_THAN_OR_EQUAL"],
"Values": ["5"]
}
}
}'
Tagging Compliance Remediation
Why Tag? Untagged resources make cost allocation impossible, prevent automated scheduling (stop dev at night), and create blind spots in security audits. Tagging is the foundation of FinOps governance.
11. Find and Tag All Untagged Resources (Bulk Operation)
Use AWS Resource Groups Tagging API to identify untagged resources and apply tags in bulk. Works across all supported resource types in any region.
aws resourcegroupstaggingapi get-resources \
--region <REGION> \
--query "ResourceTagMappingList[?Tags==\`[]\`].ResourceARN" \
--output table
aws resourcegroupstaggingapi get-resources \
--region <REGION> \
--query "ResourceTagMappingList[?!contains(Tags[].Key, 'Environment')].ResourceARN" \
--output table
aws resourcegroupstaggingapi tag-resources \
--region <REGION> \
--resource-arn-list \
"arn:aws:ec2:<REGION>:<ACCOUNT>:instance/<INSTANCE_ID>" \
"arn:aws:ec2:<REGION>:<ACCOUNT>:volume/<VOLUME_ID>" \
--tags Environment=production,Owner=platform-team,Project=main-app,CostCenter=CC-1234
12. Activate Cost Allocation Tags in Billing
Tags only appear in Cost Explorer reports after being activated as cost allocation tags. This is a one-time setup per tag key.
aws ce list-cost-allocation-tags --status Active \
--query "CostAllocationTags[].{Key:TagKey,Status:Status,Type:Type}" \
--output table
aws ce update-cost-allocation-tags-status \
--cost-allocation-tags-status '[
{"TagKey": "Environment", "Status": "Active"},
{"TagKey": "Owner", "Status": "Active"},
{"TagKey": "Project", "Status": "Active"},
{"TagKey": "CostCenter", "Status": "Active"}
]'
13. Deploy AWS Config Rule for Tag Compliance
Deploy the required-tags Config rule to continuously monitor and alert on resources missing required tags. Works across all regions.
aws configservice put-config-rule --region <REGION> \
--config-rule '{
"ConfigRuleName": "required-tags-compliance",
"Description": "Checks resources have required cost allocation tags",
"Source": {
"Owner": "AWS",
"SourceIdentifier": "REQUIRED_TAGS"
},
"InputParameters": "{\"tag1Key\":\"Environment\",\"tag2Key\":\"Owner\",\"tag3Key\":\"Project\",\"tag4Key\":\"CostCenter\"}",
"Scope": {
"ComplianceResourceTypes": [
"AWS::EC2::Instance",
"AWS::EC2::Volume",
"AWS::S3::Bucket",
"AWS::RDS::DBInstance",
"AWS::Lambda::Function",
"AWS::ElasticLoadBalancingV2::LoadBalancer"
]
}
}'
aws configservice get-compliance-details-by-config-rule \
--region <REGION> \
--config-rule-name required-tags-compliance \
--compliance-types NON_COMPLIANT \
--query "EvaluationResults[].{Resource:EvaluationResultIdentifier.EvaluationResultQualifier.ResourceId,Type:EvaluationResultIdentifier.EvaluationResultQualifier.ResourceType}" \
--output table
14. Enforce Tags via SCP (Deny Untagged Resource Creation)
Prevent resources from being created without required tags using a Service Control Policy. Apply to production OUs to enforce governance at the organizational level.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyEC2WithoutTags",
"Effect": "Deny",
"Action": ["ec2:RunInstances"],
"Resource": ["arn:aws:ec2:*:*:instance/*"],
"Condition": {
"Null": {
"aws:RequestTag/Environment": "true",
"aws:RequestTag/Owner": "true"
}
}
},
{
"Sid": "DenyRDSWithoutTags",
"Effect": "Deny",
"Action": ["rds:CreateDBInstance"],
"Resource": ["*"],
"Condition": {
"Null": {
"aws:RequestTag/Environment": "true",
"aws:RequestTag/Owner": "true"
}
}
}
]
}
aws organizations create-policy \
--name "RequireTagsOnCreate" \
--type SERVICE_CONTROL_POLICY \
--description "Deny resource creation without required tags" \
--content file://deny-untagged.json
aws organizations attach-policy \
--policy-id <POLICY_ID> \
--target-id <OU_ID>
15. Auto-Tag Resources at Creation (EventBridge + Lambda)
Automatically apply Creator, CreatedDate, and default tags when resources are created using CloudTrail events. Ensures no resource goes untagged.
aws events put-rule --region <REGION> \
--name "AutoTagOnCreate" \
--event-pattern '{
"source": ["aws.ec2", "aws.rds", "aws.s3"],
"detail-type": ["AWS API Call via CloudTrail"],
"detail": {
"eventName": ["RunInstances", "CreateDBInstance", "CreateBucket", "CreateVolume"]
}
}' \
--state ENABLED
# auto_tag_lambda.py - Deploy as Lambda function
import boto3, json
from datetime import datetime
def handler(event, context):
detail = event.get('detail', {})
user = detail.get('userIdentity', {}).get('arn', 'unknown')
region = event.get('region', 'us-east-1')
# Extract resource ARNs from the event
resources = event.get('resources', [])
tagging = boto3.client('resourcegroupstaggingapi', region_name=region)
tags = {
'Creator': user.split('/')[-1],
'CreatedDate': datetime.utcnow().strftime('%Y-%m-%d'),
'ManagedBy': 'auto-tagger'
}
for arn in resources:
try:
tagging.tag_resources(ResourceARNList=[arn], Tags=tags)
except Exception as e:
print(f"Failed to tag {arn}: {e}")
16. Standardize Tag Values with Tag Editor
Use AWS Tag Editor to find inconsistent tag values (e.g., "prod" vs "production" vs "Production") and normalize them across all resources in a region.
aws resourcegroupstaggingapi get-tag-values \
--region <REGION> \
--key "Environment"
# Find resources with incorrect value
ARNS=$(aws resourcegroupstaggingapi get-resources \
--region <REGION> \
--tag-filters Key=Environment,Values=prod \
--query "ResourceTagMappingList[].ResourceARN" --output text)
# Re-tag with correct value
for arn in $ARNS; do
aws resourcegroupstaggingapi tag-resources \
--region <REGION> \
--resource-arn-list "$arn" \
--tags Environment=production
done
for REGION in us-east-1 us-west-2 eu-west-1 ap-southeast-1; do
echo "Scanning $REGION..."
aws resourcegroupstaggingapi get-resources \
--region $REGION \
--query "ResourceTagMappingList[?Tags==\`[]\`].ResourceARN" \
--output text | wc -w
done
AI/ML Cost Remediation (FinOps for AI)
AI/ML services are among the highest-cost AWS resources. SageMaker endpoints bill per hour (even idle), Bedrock bills per token, and training jobs can cost hundreds per run. These remediations target the most common waste patterns.
17. Delete Idle SageMaker Endpoints ($50-500/mo each)
SageMaker real-time endpoints bill per instance-hour even with zero invocations. Identify endpoints with no traffic and delete them, or switch to serverless inference.
aws sagemaker list-endpoints --region <REGION> \
--status-equals InService \
--query "Endpoints[].{Name:EndpointName,Created:CreationTime}" \
--output table
aws cloudwatch get-metric-statistics --region <REGION> \
--namespace AWS/SageMaker \
--metric-name Invocations \
--dimensions Name=EndpointName,Value=<ENDPOINT_NAME> \
Name=VariantName,Value=AllTraffic \
--start-time $(date -u -d '7 days ago' +%Y-%m-%dT%H:%M:%S) \
--end-time $(date -u +%Y-%m-%dT%H:%M:%S) \
--period 86400 --statistics Sum \
--query "Datapoints[].Sum" --output text
aws sagemaker delete-endpoint --region <REGION> \
--endpoint-name <ENDPOINT_NAME>
# Also delete the endpoint config
aws sagemaker delete-endpoint-config --region <REGION> \
--endpoint-config-name <ENDPOINT_NAME>
18. Stop Idle SageMaker Notebook Instances ($50-200/mo each)
Notebook instances bill per hour while in "InService" state, even when no user is connected. Stop them when not actively being used and set up auto-stop lifecycle configs.
aws sagemaker list-notebook-instances --region <REGION> \
--status-equals InService \
--query "NotebookInstances[].{Name:NotebookInstanceName,Type:InstanceType,Status:NotebookInstanceStatus}" \
--output table
aws sagemaker stop-notebook-instance --region <REGION> \
--notebook-instance-name <NOTEBOOK_NAME>
# Create the lifecycle config script
cat << 'EOF' > auto-stop.sh
#!/bin/bash
IDLE_TIME=3600
echo "Auto-stop lifecycle config: will stop after ${IDLE_TIME}s idle"
nohup sh -c "while true; do
LAST_ACTIVITY=$(stat -c %Y /home/ec2-user/SageMaker/ 2>/dev/null || echo 0)
NOW=$(date +%s)
IDLE=$((NOW - LAST_ACTIVITY))
if [ $IDLE -gt $IDLE_TIME ]; then
echo 'Notebook idle, initiating stop...'
aws sagemaker stop-notebook-instance --notebook-instance-name $(jq -r '.ResourceName' /opt/ml/metadata/resource-metadata.json)
exit 0
fi
sleep 300
done" &
EOF
aws sagemaker create-notebook-instance-lifecycle-config \
--region <REGION> \
--notebook-instance-lifecycle-config-name auto-stop-idle \
--on-start Content=$(base64 auto-stop.sh)
19. Enable SageMaker Managed Spot Training (up to 90% savings)
Use EC2 Spot Instances for SageMaker training jobs. Add checkpointing to handle interruptions gracefully. Saves up to 90% vs on-demand training costs.
# In your training job configuration, add:
training_params = {
'TrainingJobName': 'my-training-job',
'EnableManagedSpotTraining': True,
'StoppingCondition': {
'MaxRuntimeInSeconds': 86400,
'MaxWaitTimeInSeconds': 172800 # Max wait for spot capacity
},
'CheckpointConfig': {
'S3Uri': 's3://<BUCKET>/checkpoints/my-training-job/'
},
# ... other params
}
# CLI equivalent:
aws sagemaker create-training-job --region <REGION> \
--training-job-name my-spot-job \
--enable-managed-spot-training \
--stopping-condition MaxRuntimeInSeconds=86400,MaxWaitTimeInSeconds=172800 \
--checkpoint-config S3Uri=s3://<BUCKET>/checkpoints/ \
--resource-config InstanceType=ml.p3.2xlarge,InstanceCount=1,VolumeSizeInGB=50 \
--algorithm-specification TrainingImage=<IMAGE_URI>,TrainingInputMode=File \
--role-arn arn:aws:iam::<ACCOUNT>:role/SageMakerExecutionRole \
--input-data-config '[...]' \
--output-data-config S3OutputPath=s3://<BUCKET>/output/
20. Configure SageMaker Endpoint Auto-Scaling
Scale SageMaker endpoint instances based on invocation metrics. Scale in during low-traffic periods to reduce hosting costs by 30-60%.
aws application-autoscaling register-scalable-target \
--region <REGION> \
--service-namespace sagemaker \
--resource-id endpoint/<ENDPOINT_NAME>/variant/AllTraffic \
--scalable-dimension sagemaker:variant:DesiredInstanceCount \
--min-capacity 1 \
--max-capacity 4
aws application-autoscaling put-scaling-policy \
--region <REGION> \
--policy-name sagemaker-scaling-policy \
--service-namespace sagemaker \
--resource-id endpoint/<ENDPOINT_NAME>/variant/AllTraffic \
--scalable-dimension sagemaker:variant:DesiredInstanceCount \
--policy-type TargetTrackingScaling \
--target-tracking-scaling-policy-configuration '{
"TargetValue": 70.0,
"PredefinedMetricSpecification": {
"PredefinedMetricType": "SageMakerVariantInvocationsPerInstance"
},
"ScaleInCooldown": 300,
"ScaleOutCooldown": 60
}'
21. Switch Low-Traffic Endpoints to Serverless Inference
SageMaker Serverless Inference scales to zero when there's no traffic - you only pay per invocation. Ideal for endpoints with bursty or low traffic patterns.
aws sagemaker create-endpoint-config --region <REGION> \
--endpoint-config-name <ENDPOINT_NAME>-serverless \
--production-variants '[{
"VariantName": "AllTraffic",
"ModelName": "<MODEL_NAME>",
"ServerlessConfig": {
"MemorySizeInMB": 2048,
"MaxConcurrency": 5
}
}]'
aws sagemaker update-endpoint --region <REGION> \
--endpoint-name <ENDPOINT_NAME> \
--endpoint-config-name <ENDPOINT_NAME>-serverless
22. Optimize Bedrock Token Usage (Reduce Input/Output Costs)
Bedrock charges per input and output token. Optimize prompts to be concise, set max_tokens limits, and use smaller models for simpler tasks.
aws cloudwatch get-metric-statistics --region <REGION> \
--namespace AWS/Bedrock \
--metric-name InputTokenCount \
--start-time $(date -u -d '7 days ago' +%Y-%m-%dT%H:%M:%S) \
--end-time $(date -u +%Y-%m-%dT%H:%M:%S) \
--period 86400 --statistics Sum \
--query "Datapoints[].{Date:Timestamp,Tokens:Sum}" --output table
aws bedrock put-model-invocation-logging-configuration \
--region <REGION> \
--logging-config '{
"cloudWatchConfig": {
"logGroupName": "/aws/bedrock/invocation-logs",
"roleArn": "arn:aws:iam::<ACCOUNT>:role/BedrockLoggingRole",
"largeDataDeliveryS3Config": {
"bucketName": "<BUCKET_NAME>",
"keyPrefix": "bedrock-logs/"
}
},
"textDataDeliveryEnabled": true,
"imageDataDeliveryEnabled": false,
"embeddingDataDeliveryEnabled": false
}'
# 1. Set explicit max_tokens in API calls:
# "max_tokens": 256 (instead of unlimited)
#
# 2. Use system prompts efficiently:
# Keep system prompts short and reusable
#
# 3. Model selection by task:
# Simple Q&A -> amazon.titan-text-lite-v1 ($0.15/1M tokens)
# Summarization -> anthropic.claude-instant ($0.80/1M tokens)
# Complex tasks -> anthropic.claude-3-sonnet ($3.00/1M tokens)
#
# 4. Implement response caching:
# Cache identical prompt responses for 5-60 minutes
23. Set Up AI/ML Budget Alerts
Create dedicated budgets for AI/ML services to get early warning on cost spikes. AI services can generate unexpected charges due to misconfigured endpoints or runaway training jobs.
aws budgets create-budget --region us-east-1 \
--account-id $AWS_ACCOUNT_ID \
--budget '{
"BudgetName": "AI-ML-Monthly-Budget",
"BudgetLimit": {"Amount": "100", "Unit": "USD"},
"TimeUnit": "MONTHLY",
"BudgetType": "COST",
"CostFilters": {
"Service": [
"Amazon SageMaker",
"Amazon Bedrock",
"Amazon Rekognition",
"Amazon Comprehend",
"Amazon Textract"
]
}
}' \
--notifications-with-subscribers '[
{
"Notification": {
"NotificationType": "ACTUAL",
"ComparisonOperator": "GREATER_THAN",
"Threshold": 50
},
"Subscribers": [{"SubscriptionType": "EMAIL", "Address": "<YOUR_EMAIL>"}]
},
{
"Notification": {
"NotificationType": "ACTUAL",
"ComparisonOperator": "GREATER_THAN",
"Threshold": 80
},
"Subscribers": [{"SubscriptionType": "EMAIL", "Address": "<YOUR_EMAIL>"}]
}
]'
24. Audit AI/ML Resources Across All Regions
AI/ML resources can be accidentally left running in non-primary regions. Scan all regions to find forgotten endpoints, notebooks, and training resources.
for REGION in us-east-1 us-west-2 eu-west-1 ap-northeast-1 ap-southeast-1; do
echo "=== $REGION ==="
# Check endpoints
ENDPOINTS=$(aws sagemaker list-endpoints --region $REGION \
--status-equals InService --query "Endpoints[].EndpointName" --output text)
if [ -n "$ENDPOINTS" ]; then
echo " ENDPOINTS: $ENDPOINTS"
fi
# Check notebooks
NOTEBOOKS=$(aws sagemaker list-notebook-instances --region $REGION \
--status-equals InService --query "NotebookInstances[].NotebookInstanceName" --output text)
if [ -n "$NOTEBOOKS" ]; then
echo " NOTEBOOKS: $NOTEBOOKS"
fi
# Check running training jobs
TRAINING=$(aws sagemaker list-training-jobs --region $REGION \
--status-equals InProgress --query "TrainingJobSummaries[].TrainingJobName" --output text)
if [ -n "$TRAINING" ]; then
echo " TRAINING JOBS: $TRAINING"
fi
done
aws ce get-cost-and-usage \
--time-period Start=$(date -u -d '30 days ago' +%Y-%m-%d),End=$(date -u +%Y-%m-%d) \
--granularity MONTHLY \
--metrics UnblendedCost \
--group-by Type=DIMENSION,Key=REGION \
--filter '{
"Dimensions": {
"Key": "SERVICE",
"Values": ["Amazon SageMaker", "Amazon Bedrock", "Amazon Rekognition",
"Amazon Comprehend", "Amazon Textract"]
}
}' \
--query "ResultsByTime[].Groups[?Metrics.UnblendedCost.Amount>\`0\`].{Region:Keys[0],Cost:Metrics.UnblendedCost.Amount}" \
--output table