Multi-Cloud Cost Optimization

Last scan: 2026-10-31T05:28:30Z
Cost Overview Optimization Anomalies Governance Tagging FinOpsForAI Container Cost Optimization Recommendations Remediation Architecture FinOps Agent
Severity: Critical High Medium Low
Remediation Actions
24
Ready-to-execute CLI commands
Estimated Monthly Savings
$45+
After all remediations applied
Quick Wins (< 5 min)
6
Single CLI command fixes
Risk Level
Low
All actions are reversible

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)

HIGH SAVINGS

1. Delete Unused CloudWatch Dashboards ($12/month savings)

CloudWatch dashboards cost $3/month each. Remove dashboards no longer actively used for monitoring.

Identify existing dashboards:
aws cloudwatch list-dashboards --region us-east-1 \
  --query "DashboardEntries[].DashboardName" --output table
Delete unused dashboards:
aws cloudwatch delete-dashboards --region us-east-1 \
  --dashboard-names "Dashboard1" "Dashboard2" "Dashboard3"
HIGH SAVINGS

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.

Stop instance (when collection complete):
aws ec2 stop-instances --region us-east-1 \
  --instance-ids <INSTANCE_ID>
Start instance (before next collection):
aws ec2 start-instances --region us-east-1 \
  --instance-ids <INSTANCE_ID>
Automate with EventBridge schedule (stop every night at 10pm):
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
HIGH SAVINGS

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.

Find unassociated Elastic IPs:
aws ec2 describe-addresses --region us-east-1 \
  --query "Addresses[?AssociationId==null].{IP:PublicIp,AllocId:AllocationId}" \
  --output table
Release an unused Elastic IP:
aws ec2 release-address --region us-east-1 \
  --allocation-id <ALLOCATION_ID>
MED SAVINGS

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.

Find log groups without retention:
aws logs describe-log-groups --region us-east-1 \
  --query "logGroups[?retentionInDays==null].{Name:logGroupName,Size:storedBytes}" \
  --output table
Set 7-day retention on non-critical logs:
# 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
Bulk set retention on all log groups (30 days):
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
MED SAVINGS

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.

Find unattached EBS volumes:
aws ec2 describe-volumes --region us-east-1 \
  --filters "Name=status,Values=available" \
  --query "Volumes[].{ID:VolumeId,Size:Size,Type:VolumeType}" \
  --output table
Snapshot before deleting (safety):
aws ec2 create-snapshot --region us-east-1 \
  --volume-id <VOLUME_ID> \
  --description "Backup before deletion"
Delete unattached volume:
aws ec2 delete-volume --region us-east-1 \
  --volume-id <VOLUME_ID>
MED SAVINGS

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%.

Apply lifecycle policy to a bucket:
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}
    }]
  }'
MED SAVINGS

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.

Enable EBS encryption by default:
aws ec2 enable-ebs-encryption-by-default --region us-east-1
Verify it's enabled:
aws ec2 get-ebs-encryption-by-default --region us-east-1
OPTIMIZATION

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.

Create S3 VPC Endpoint:
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
OPTIMIZATION

9. Set Up AWS Budget Alerts (Prevent Surprise Bills)

Create budget alerts that notify you when spending exceeds thresholds, preventing unexpected cost overruns.

Create a monthly budget with email alert at 80%:
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>"
    }]
  }]'
OPTIMIZATION

10. Enable Cost Anomaly Detection (Automated Monitoring)

Automatically detect unusual cost patterns and get notified before small anomalies become large surprises.

Create a cost anomaly monitor:
aws ce create-anomaly-monitor --region us-east-1 \
  --anomaly-monitor '{
    "MonitorName": "AllServices-Monitor",
    "MonitorType": "DIMENSIONAL",
    "MonitorDimension": "SERVICE"
  }'
Create subscription for email alerts:
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.

HIGH PRIORITY

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.

Find all resources with NO tags in a region:
aws resourcegroupstaggingapi get-resources \
  --region <REGION> \
  --query "ResourceTagMappingList[?Tags==\`[]\`].ResourceARN" \
  --output table
Find resources missing a specific tag (e.g., Environment):
aws resourcegroupstaggingapi get-resources \
  --region <REGION> \
  --query "ResourceTagMappingList[?!contains(Tags[].Key, 'Environment')].ResourceARN" \
  --output table
Bulk-tag multiple resources at once:
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
HIGH PRIORITY

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.

List existing cost allocation tags:
aws ce list-cost-allocation-tags --status Active \
  --query "CostAllocationTags[].{Key:TagKey,Status:Status,Type:Type}" \
  --output table
Activate user-defined tags for cost allocation:
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"}
  ]'
HIGH PRIORITY

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.

Create required-tags Config rule:
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"
      ]
    }
  }'
Check compliance status:
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
MED PRIORITY

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.

Create SCP policy JSON (save as deny-untagged.json):
{
  "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"
        }
      }
    }
  ]
}
Attach SCP to an OU:
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>
MED PRIORITY

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.

Create EventBridge rule to capture resource creation:
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
Lambda function logic (Python):
# 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}")
GOVERNANCE

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.

Find all unique values for a tag key across resources:
aws resourcegroupstaggingapi get-tag-values \
  --region <REGION> \
  --key "Environment"
Fix inconsistent tags (rename "prod" to "production"):
# 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
Bulk tag across all regions (script):
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.

HIGH SAVINGS

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.

List all InService endpoints in a region:
aws sagemaker list-endpoints --region <REGION> \
  --status-equals InService \
  --query "Endpoints[].{Name:EndpointName,Created:CreationTime}" \
  --output table
Check invocation metrics (last 7 days) for an endpoint:
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
Delete an idle endpoint:
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>
HIGH SAVINGS

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.

Find running notebook instances:
aws sagemaker list-notebook-instances --region <REGION> \
  --status-equals InService \
  --query "NotebookInstances[].{Name:NotebookInstanceName,Type:InstanceType,Status:NotebookInstanceStatus}" \
  --output table
Stop a notebook instance:
aws sagemaker stop-notebook-instance --region <REGION> \
  --notebook-instance-name <NOTEBOOK_NAME>
Create auto-stop lifecycle config (stops after 1 hour idle):
# 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)
HIGH SAVINGS

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.

Create training job with Spot Instances (Python/boto3):
# 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/
MED SAVINGS

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%.

Register endpoint as scalable target:
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
Create scaling policy (target tracking on invocations):
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
  }'
MED SAVINGS

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.

Create serverless endpoint config:
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
    }
  }]'
Update existing endpoint to serverless:
aws sagemaker update-endpoint --region <REGION> \
  --endpoint-name <ENDPOINT_NAME> \
  --endpoint-config-name <ENDPOINT_NAME>-serverless
MED SAVINGS

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.

Monitor Bedrock invocation costs via CloudWatch:
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
Set up Bedrock model invocation logging for cost tracking:
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
  }'
Best practices for token reduction:
# 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
GOVERNANCE

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.

Create AI/ML-specific budget with service filter:
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>"}]
    }
  ]'
GOVERNANCE

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.

Scan all regions for running SageMaker 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
Get AI/ML costs by region from Cost Explorer:
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