Terraform Cost Reporter Script
Published on 2025-05-16 by Mathieu
Terraform Cost Reporter Script
A simple script to calculate infrastructure costs for your Terraform projects and send detailed reports via email.
Overview
This script allows you to:
- Generate cost estimates for your Terraform project using Infracost
- Create a detailed HTML report with cost breakdowns
- Send the report via email to stakeholders
Prerequisites
- Python 3.6+
- Terraform installed locally
- Infracost CLI installed (Installation Guide)
- An Infracost API key (Sign up)
- Email account for sending reports
Quick Start
- Clone this repository or download the files
- Install the required Python packages:
pip install -r requirements.txt
- Set up your configuration in
config.ini
- Run the script:
python terraform_cost_reporter.py /path/to/terraform/project
Files Overview
terraform_cost_reporter.py
#!/usr/bin/env python3
"""
Terraform Cost Reporter
Generates cost estimates for Terraform projects and sends reports via email.
"""
import argparse
import configparser
import json
import os
import subprocess
import sys
from email import encoders
from email.mime.base import MIMEBase
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
import smtplib
import ssl
import tempfile
def read_config(config_path='config.ini'):
"""Read configuration from config.ini file"""
if not os.path.exists(config_path):
print(f"Error: Configuration file '{config_path}' not found.")
print("Please create a config.ini file based on config.ini.example")
sys.exit(1)
config = configparser.ConfigParser()
config.read(config_path)
return config
def run_infracost(terraform_dir, project_name, environment):
"""Run Infracost and generate cost reports"""
# Create temp directory for outputs
temp_dir = tempfile.mkdtemp()
txt_report = os.path.join(temp_dir, f"infracost-{project_name}-{environment}.txt")
json_report = os.path.join(temp_dir, f"infracost-{project_name}-{environment}.json")
# Make sure terraform dir exists
if not os.path.isdir(terraform_dir):
print(f"Error: Terraform directory '{terraform_dir}' not found.")
sys.exit(1)
# Check if Infracost is installed
try:
subprocess.run(['infracost', '--version'], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
except (subprocess.SubprocessError, FileNotFoundError):
print("Error: Infracost not found. Please install Infracost CLI.")
print("Visit: https://www.infracost.io/docs/#installation")
sys.exit(1)
# Set currency
subprocess.run(['infracost', 'configure', 'set', 'currency', 'EUR'], check=True)
# Run terraform init if .terraform directory doesn't exist
terraform_initialized = os.path.isdir(os.path.join(terraform_dir, '.terraform'))
if not terraform_initialized:
print("Running 'terraform init'...")
subprocess.run(['terraform', 'init'], cwd=terraform_dir, check=True)
# Generate terraform plan in temporary file
plan_file = os.path.join(temp_dir, "tfplan")
print(f"Generating Terraform plan for {project_name} in {environment} environment...")
try:
subprocess.run(
['terraform', 'plan', '-out', plan_file],
cwd=terraform_dir,
check=True
)
except subprocess.SubprocessError as e:
print(f"Error generating Terraform plan: {e}")
sys.exit(1)
# Run Infracost breakdown and save reports
print("Generating Infracost breakdown...")
try:
# Generate text report
subprocess.run([
'infracost', 'breakdown',
'--path', plan_file,
'--format', 'table',
'--out-file', txt_report
], check=True)
# Generate JSON report
subprocess.run([
'infracost', 'breakdown',
'--path', plan_file,
'--format', 'json',
'--out-file', json_report
], check=True)
print(f"Infracost reports generated:")
print(f" - Text report: {txt_report}")
print(f" - JSON report: {json_report}")
return txt_report, json_report
except subprocess.SubprocessError as e:
print(f"Error running Infracost: {e}")
sys.exit(1)
def create_html_email_body(json_data, project_name, environment):
"""Create HTML email body from Infracost JSON data"""
try:
# Load the project from the first element if it exists
project = json_data.get('projects', [{}])[0]
currency = json_data.get('currency', '€')
html_body = f"""
<html>
<body>
<p>Hello,</p>
<p>Please find below the Infracost estimate for the application: <b>{project_name}</b> infrastructure in the <b>{environment}</b> environment:</p>
<h3>📊 Summary Breakdown</h3>
<table border="1" style="border-collapse: collapse; width: 100%;">
<tr style="background-color: #f2f2f2;">
<th style="padding: 8px;">Project</th>
<th style="padding: 8px;">Baseline Cost ({currency})</th>
<th style="padding: 8px;">Usage Cost* ({currency})</th>
<th style="padding: 8px;">Total Cost ({currency})</th>
</tr>
"""
breakdown = project.get('breakdown', {})
total_cost = float(breakdown.get('totalMonthlyCost') or 0.0)
usage_cost = float(breakdown.get('totalMonthlyUsageCost') or 0.0)
baseline_cost = total_cost - usage_cost
if float(total_cost) == 0:
html_body += """
<tr>
<td style="padding: 8px;" colspan="4">No cost information available</td>
</tr>
"""
else:
html_body += f"""
<tr>
<td style="padding: 8px;">{project.get('name', project_name)}</td>
<td style="padding: 8px;">{currency}{format(float(baseline_cost), '.2f')}</td>
<td style="padding: 8px;">{currency}{format(float(usage_cost), '.2f')}</td>
<td style="padding: 8px;">{currency}{format(float(total_cost), '.2f')}</td>
</tr>
"""
html_body += f"""
</table>
<h3>📋 Detailed Resources Breakdown</h3>
<table border="1" style="border-collapse: collapse; width: 100%;">
<tr style="background-color: #f2f2f2;">
<th style="padding: 8px;">Name</th>
<th style="padding: 8px;">Monthly Quantity</th>
<th style="padding: 8px;">Unit</th>
<th style="padding: 8px;">Monthly Cost ({currency})</th>
</tr>
"""
resources = breakdown.get('resources', [])
if not resources:
html_body += """
<tr>
<td style="padding: 8px;" colspan="4">No resources breakdown found</td>
</tr>
"""
else:
for resource in resources:
for component in resource.get('costComponents', []):
name = component.get('name', 'Unknown')
unit = component.get('unit', 'N/A')
quantity = component.get('monthlyQuantity') or 0.0
cost = component.get('monthlyCost') or 0.0
html_body += f"""
<tr>
<td style="padding: 8px;">{name}</td>
<td style="padding: 8px;">{format(float(quantity), ',')}</td>
<td style="padding: 8px;">{unit}</td>
<td style="padding: 8px;">{currency}{format(float(cost), '.2f')}</td>
</tr>
"""
html_body += f"""
</table>
<h3>📈 Resource Summary</h3>
<table border="1" style="border-collapse: collapse; width: 100%;">
<tr style="background-color: #f2f2f2;">
<th style="padding: 8px;">Total Detected Resources</th>
<th style="padding: 8px;">Total Supported</th>
<th style="padding: 8px;">Total Unsupported</th>
<th style="padding: 8px;">Total No-Price</th>
</tr>
"""
summary = json_data.get('summary', {})
total_detected = summary.get('totalDetectedResources', 0)
total_supported = summary.get('totalSupportedResources', 0)
total_unsupported = summary.get('totalUnsupportedResources', 0)
total_no_price = summary.get('totalNoPriceResources', 0)
html_body += f"""
<tr>
<td style="padding: 8px;">{total_detected}</td>
<td style="padding: 8px;">{total_supported}</td>
<td style="padding: 8px;">{total_unsupported}</td>
<td style="padding: 8px;">{total_no_price}</td>
</tr>
</table>
<p>* Usage-based costs are estimated and may vary.</p>
</body>
</html>
"""
return html_body
except Exception as e:
print(f"Error creating HTML email body: {e}")
# Return a simple fallback message
return f"<html><body><p>Infrastructure cost report for {project_name} in {environment} environment.</p></body></html>"
def send_email(html_body, txt_report_path, config, project_name, environment, total_cost, currency='€'):
"""Send email with cost report"""
try:
smtp_server = config['Email']['smtp_server']
smtp_port = int(config['Email']['smtp_port'])
smtp_username = config['Email']['smtp_username']
smtp_password = config['Email']['smtp_password']
sender_email = config['Email']['sender_email']
receiver_email = config['Email']['receiver_email']
# Construct the email
message = MIMEMultipart("alternative")
subject = f"Infrastructure Cost Estimation - {project_name.upper()} ({environment}) - {currency}{total_cost}/month"
message["Subject"] = subject
message["From"] = sender_email
message["To"] = receiver_email
# Attach the HTML content
html_part = MIMEText(html_body, "html")
message.attach(html_part)
# Attach the text report as a file
with open(txt_report_path, "rb") as attachment:
part = MIMEBase("application", "octet-stream")
part.set_payload(attachment.read())
encoders.encode_base64(part)
part.add_header(
"Content-Disposition",
f"attachment; filename= {os.path.basename(txt_report_path)}",
)
message.attach(part)
# Establish a secure connection with the server and send email
context = ssl.create_default_context()
with smtplib.SMTP(smtp_server, smtp_port) as server:
server.ehlo()
server.starttls(context=context)
server.ehlo()
server.login(smtp_username, smtp_password)
server.sendmail(sender_email, receiver_email, message.as_string())
print(f"Email sent successfully to {receiver_email}")
return True
except Exception as e:
print(f"Error sending email: {e}")
return False
def main():
"""Main function that runs the script"""
parser = argparse.ArgumentParser(description='Generate and email Terraform infrastructure cost reports')
parser.add_argument('terraform_dir', help='Path to the Terraform project directory')
parser.add_argument('--project', '-p', default='terraform-project', help='Project name for the report')
parser.add_argument('--env', '-e', default='dev', help='Environment name (dev, test, prod, etc.)')
parser.add_argument('--config', '-c', default='config.ini', help='Path to configuration file')
args = parser.parse_args()
# Read configuration
config = read_config(args.config)
# Set Infracost API key from config
os.environ['INFRACOST_API_KEY'] = config['Infracost']['api_key']
# Run Infracost to generate reports
txt_report, json_report = run_infracost(args.terraform_dir, args.project, args.env)
# Read the JSON report
with open(json_report, 'r') as file:
json_data = json.load(file)
# Create HTML email body
html_body = create_html_email_body(json_data, args.project, args.env)
# Get total cost for subject line
total_cost = format(float(json_data.get('totalMonthlyCost', 0.0)), '.2f')
currency = json_data.get('currency', '€')
# Send email
print("Sending cost report email...")
success = send_email(html_body, txt_report, config, args.project, args.env, total_cost, currency)
if success:
print("✅ Cost report generated and sent successfully!")
else:
print("❌ Failed to send email, but cost reports were generated.")
print(f"Text report available at: {txt_report}")
print(f"JSON report available at: {json_report}")
if __name__ == "__main__":
main()
config.ini.example
[Infracost]
api_key = your_infracost_api_key_here
[Email]
smtp_server = smtp.gmail.com
smtp_port = 587
smtp_username = your_email@gmail.com
smtp_password = your_email_password_or_app_password
sender_email = your_email@gmail.com
receiver_email = recipient@example.com
requirements.txt
configparser>=5.0.0
Usage
Basic Usage
python terraform_cost_reporter.py /path/to/terraform/project
With Project and Environment Names
python terraform_cost_reporter.py /path/to/terraform/project --project my-api --env production
Using a Different Config File
python terraform_cost_reporter.py /path/to/terraform/project --config my-config.ini
Sample Email Output
When run successfully, this script will send an email that looks like:
Subject: Infrastructure Cost Estimation - MY-PROJECT (production) - €123.45/month
Hello,
Please find below the Infracost estimate for the application: my-project infrastructure in the production environment:
📊 Summary Breakdown
[Table with baseline, usage, and total costs]
📋 Detailed Resources Breakdown
[Table listing all resources and their individual costs]
📈 Resource Summary
[Table showing resource statistics]
* Usage-based costs are estimated and may vary.
Customizing the Script
Changing Currency
To change the currency (default is EUR), modify line 61 in the script:
# Change EUR to USD, GBP, etc.
subprocess.run(['infracost', 'configure', 'set', 'currency', 'EUR'], check=True)
Adding Authentication for Cloud Providers
For AWS, Azure, or GCP authentication, set the appropriate environment variables before running the script:
AWS Example:
export AWS_ACCESS_KEY_ID=your_access_key
export AWS_SECRET_ACCESS_KEY=your_secret_key
export AWS_REGION=your_region
python terraform_cost_reporter.py /path/to/terraform/project
Azure Example:
export ARM_CLIENT_ID=your_client_id
export ARM_CLIENT_SECRET=your_client_secret
export ARM_SUBSCRIPTION_ID=your_subscription_id
export ARM_TENANT_ID=your_tenant_id
python terraform_cost_reporter.py /path/to/terraform/project
Scheduled Execution
You can set up a cron job to run this script regularly:
# Run every Monday at 8 AM
0 8 * * 1 /path/to/python /path/to/terraform_cost_reporter.py /path/to/terraform/project
Troubleshooting
SMTP Authentication Issues
If you're using Gmail, you might need to:
- Enable "Less secure app access" in your Google account
- Or (preferred) create an "App Password" for this script
Infracost API Key Issues
Make sure your Infracost API key is valid and properly set in your config.ini file. You can verify your API key with:
infracost configure get api_key
Terraform Plan Errors
If you encounter errors during Terraform plan generation:
- Try running
terraform plan
manually in your project directory - Verify your Terraform version is compatible with your configuration
- Check if you need to set cloud provider credentials
License
This script is available under the MIT License. Feel free to use and modify it for your needs.
Tags: infracost, cloud, finops