This second-stage Lambda processes the DataSync execution report from S3, estimates transfer/request costs, builds a readable summary, and publishes the report to SNS for operational visibility.
Input and Environment Configuration
# Report Lambda
# Generate Report and send it to SNS
# Lambda Env Variable
DATASYNC_COST_PER_GB: 0.015
DATASYNC_TASK_EXECUTION_COST: 0.55
DATA_TRANSFER_OUT_FIRST_10TB_PER_GB: 0.09
DATA_TRANSFER_OUT_GREATER_150TB_PER_GB: 0.05
DATA_TRANSFER_OUT_NEXT_100TB_PER_GB: 0.07
DATA_TRANSFER_OUT_NEXT_40TB_PER_GB: 0.085
S3_GET_COST_PER_1K: 0.0004
S3_HEAD_COST_PER_1K: 0.0004
S3_LIST_COST_PER_1K: 0.005
S3_PUT_COST_PER_1K: 0.005
SNS_TOPIC_ARN: arn:aws:sns:{region}:{account_id}:{SNS_TOPIC_NAME}
Report Lambda (Python)
import os
import json
import boto3
import logging
import math
logger = logging.getLogger()
logger.setLevel(logging.INFO)
s3_client = boto3.client('s3')
sns_client = boto3.client('sns')
# Constants (as of July 2025, Enhanced mode)
DATASYNC_COST_PER_GB = float(os.environ.get('DATASYNC_COST_PER_GB', '0.015'))
DATASYNC_TASK_EXECUTION_COST = float(os.environ.get('DATASYNC_TASK_EXECUTION_COST', '0.55'))
# S3 request costs
S3_LIST_COST_PER_1K = float(os.environ.get('S3_LIST_COST_PER_1K', '0.005'))
S3_HEAD_COST_PER_1K = float(os.environ.get('S3_HEAD_COST_PER_1K', '0.0004'))
S3_GET_COST_PER_1K = float(os.environ.get('S3_GET_COST_PER_1K', '0.0004'))
S3_PUT_COST_PER_1K = float(os.environ.get('S3_PUT_COST_PER_1K', '0.005'))
S3_COPY_COST_PER_1K = float(os.environ.get('S3_COPY_COST_PER_1K', '0.005'))
# Data transfer out costs
DATA_TRANSFER_OUT_FIRST_10TB_PER_GB = float(os.environ.get('DATA_TRANSFER_OUT_FIRST_10TB_PER_GB', '0.09'))
DATA_TRANSFER_OUT_NEXT_40TB_PER_GB = float(os.environ.get('DATA_TRANSFER_OUT_NEXT_40TB_PER_GB', '0.085'))
DATA_TRANSFER_OUT_NEXT_100TB_PER_GB = float(os.environ.get('DATA_TRANSFER_OUT_NEXT_100TB_PER_GB', '0.07'))
DATA_TRANSFER_OUT_GREATER_150TB_PER_GB = float(os.environ.get('DATA_TRANSFER_OUT_GREATER_150TB_PER_GB', '0.05'))
def calculate_s3_request_cost(report):
"""Calculate S3 request costs based on DataSync report"""
result = report.get('Result', {})
files_listed = result.get('FilesListed', {})
files_transferred = result.get('FilesTransferred', 0)
source_is_s3 = report.get('SourceLocation', {}).get('LocationType') == 'Amazon S3'
dest_is_s3 = report.get('DestinationLocation', {}).get('LocationType') == 'Amazon S3'
list_requests = 0
head_requests = 0
get_requests = 0
put_requests = 0
copy_requests = 0
if source_is_s3:
list_requests += math.ceil(files_listed.get('AtSource', 0) / 1000.0)
head_requests += files_listed.get('AtSource', 0)
get_requests += files_transferred
if dest_is_s3:
list_requests += math.ceil(files_listed.get('AtDestinationForDelete', 0) / 1000.0)
head_requests += files_listed.get('AtDestinationForDelete', 0)
put_requests += files_transferred
copy_requests += files_transferred
costs = {
'LIST': (list_requests / 1000.0) * S3_LIST_COST_PER_1K,
'HEAD': (head_requests / 1000.0) * S3_HEAD_COST_PER_1K,
'GET': (get_requests / 1000.0) * S3_GET_COST_PER_1K,
'PUT': (put_requests / 1000.0) * S3_PUT_COST_PER_1K,
'COPY': (copy_requests / 1000.0) * S3_COPY_COST_PER_1K,
}
return costs, {
'LIST': list_requests,
'HEAD': head_requests,
'GET': get_requests,
'PUT': put_requests,
'COPY': copy_requests,
}
def calculate_data_transfer_out_cost(gb_transferred):
if gb_transferred <= 10240:
return gb_transferred * DATA_TRANSFER_OUT_FIRST_10TB_PER_GB
elif gb_transferred <= 51200:
return (10240 * DATA_TRANSFER_OUT_FIRST_10TB_PER_GB) + ((gb_transferred - 10240) * DATA_TRANSFER_OUT_NEXT_40TB_PER_GB)
elif gb_transferred <= 151200:
return (
(10240 * DATA_TRANSFER_OUT_FIRST_10TB_PER_GB)
+ (40960 * DATA_TRANSFER_OUT_NEXT_40TB_PER_GB)
+ ((gb_transferred - 51200) * DATA_TRANSFER_OUT_NEXT_100TB_PER_GB)
)
else:
return (
(10240 * DATA_TRANSFER_OUT_FIRST_10TB_PER_GB)
+ (40960 * DATA_TRANSFER_OUT_NEXT_40TB_PER_GB)
+ (100000 * DATA_TRANSFER_OUT_NEXT_100TB_PER_GB)
+ ((gb_transferred - 151200) * DATA_TRANSFER_OUT_GREATER_150TB_PER_GB)
)
def lambda_handler(event, context):
try:
record = event['Records'][0]
bucket = record['s3']['bucket']['name']
key = record['s3']['object']['key']
logger.info(f"Triggered by S3 object: s3://{bucket}/{key}")
except Exception as e:
logger.error(f"Could not parse S3 event: {e}")
return {"statusCode": 400, "body": "Invalid S3 event"}
try:
response = s3_client.get_object(Bucket=bucket, Key=key)
report = json.loads(response['Body'].read())
except Exception as e:
logger.error(f"Could not read or parse report: {e}")
return {"statusCode": 500, "body": "Report processing failed"}
try:
account_id = report.get('AccountId')
task_execution_id = report.get('TaskExecutionId')
source_location = report.get('SourceLocation', {})
destination_location = report.get('DestinationLocation', {})
start_time = report.get('StartTime')
end_time = report.get('EndTime')
total_time = report.get('TotalTime')
overall_status = report.get('OverallStatus')
result = report.get('Result', {})
files_transferred = result.get('FilesTransferred', 0)
bytes_transferred = result.get('BytesTransferred', 0)
source_location_type = source_location.get('LocationType', 'Unknown')
destination_location_type = destination_location.get('LocationType', 'Unknown')
files_deleted = result.get('FilesDeleted', 0)
files_verified = result.get('FilesVerified', 0)
files_skipped = result.get('FilesSkipped', 0)
error_code = result.get('ErrorCode')
error_detail = result.get('ErrorDetail')
files_listed = result.get('FilesListed', {})
except Exception as e:
logger.error(f"Could not extract fields: {e}")
return {"statusCode": 500, "body": "Field extraction failed"}
try:
gb_transferred = bytes_transferred / (1024 ** 3)
datasync_cost = gb_transferred * DATASYNC_COST_PER_GB
task_execution_cost = DATASYNC_TASK_EXECUTION_COST
s3_costs, request_counts = calculate_s3_request_cost(report)
s3_request_cost = sum(s3_costs.values())
data_transfer_out_cost = calculate_data_transfer_out_cost(gb_transferred)
total_cost = datasync_cost + task_execution_cost + s3_request_cost + data_transfer_out_cost
except Exception as e:
logger.error(f"Could not calculate cost: {e}")
return {"statusCode": 500, "body": "Cost calculation failed"}
s3_cost_details = ""
for req_type, cost in s3_costs.items():
if cost > 0:
count = request_counts[req_type]
s3_cost_details += f" {req_type} requests ({count}): \${cost:.8f}\\n"
summary = (
f"AccountId: {account_id}\\n"
f"TaskExecutionId: {task_execution_id}\\n"
f"SourceLocation: {source_location_type} ({source_location.get('LocationId', 'N/A')})\\n"
f"DestinationLocation: {destination_location_type} ({destination_location.get('LocationId', 'N/A')})\\n"
f"StartTime: {start_time}\\n"
f"EndTime: {end_time}\\n"
f"TotalTime: {total_time} ms\\n"
f"OverallStatus: {overall_status}\\n"
)
if error_code:
summary += f"ErrorCode: {error_code}\\n"
if error_detail:
summary += f"ErrorDetail: {error_detail}\\n"
summary += (
f"\\nTransfer Results:\\n"
f" FilesTransferred: {files_transferred}\\n"
f" BytesTransferred: {bytes_transferred} bytes ({gb_transferred:.2f} GB)\\n"
f" FilesDeleted: {files_deleted}\\n"
f" FilesVerified: {files_verified}\\n"
f" FilesSkipped: {files_skipped}\\n"
f" FilesListed (Source): {files_listed.get('AtSource', 0)}\\n"
f" FilesListed (Destination): {files_listed.get('AtDestinationForDelete', 0)}\\n"
f"\\nEstimated Cost Breakdown:\\n"
f" DataSync Costs:\\n"
f" Data transferred: \${datasync_cost:.6f} ({gb_transferred:.2f} GB ร \${DATASYNC_COST_PER_GB}/GB)\\n"
f" Task executions: \${task_execution_cost:.2f}\\n"
)
if s3_request_cost > 0:
summary += f" S3 Request Charges:\\n{s3_cost_details}"
else:
summary += " S3 Request Charges: \$0.00\\n"
summary += (
f" Data transfer out (to Internet): \${data_transfer_out_cost:.6f}\\n"
f" TOTAL ESTIMATED COST: \${total_cost:.6f}\\n\\n"
)
logger.info(f"Summary:\\n{summary}")
try:
sns_topic_arn = os.environ['SNS_TOPIC_ARN']
status_indicator = "SUCCESS" if overall_status == "SUCCESS" else "ERROR"
sns_client.publish(
TopicArn=sns_topic_arn,
Subject=f"AWS DataSync Task Report - {task_execution_id} [{status_indicator}]",
Message=summary,
)
logger.info("Report sent to SNS.")
except Exception as e:
logger.error(f"Could not send SNS notification: {e}")
return {"statusCode": 500, "body": "SNS notification failed"}
return {"statusCode": 200, "body": "Report processed and sent."}
Operational Notes
- Cost model values are configurable via env vars and should be reviewed when pricing changes.
- Ensure S3 trigger points only to validated DataSync report objects.
- SNS topic policy must allow Lambda publish in your account/region.
- Consider adding CloudWatch metrics for cost anomalies and repeated failures.
Official Documentation
- AWS DataSync User Guide: https://docs.aws.amazon.com/datasync/latest/userguide/what-is-datasync.html
- AWS DataSync Task Reports: https://docs.aws.amazon.com/datasync/latest/userguide/task-reports.html
- Amazon SNS Developer Guide: https://docs.aws.amazon.com/sns/latest/dg/welcome.html
- AWS Lambda Developer Guide: https://docs.aws.amazon.com/lambda/latest/dg/welcome.html
