tfcost 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
tfcost-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,9 @@
1
+ Metadata-Version: 2.4
2
+ Name: tfcost
3
+ Version: 0.1.0
4
+ Summary: Terraform cost estimation tool
5
+ Author: Rakshith Jakkani
6
+ Requires-Dist: boto3
7
+ Dynamic: author
8
+ Dynamic: requires-dist
9
+ Dynamic: summary
tfcost-0.1.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
tfcost-0.1.0/setup.py ADDED
@@ -0,0 +1,17 @@
1
+ from setuptools import setup, find_packages
2
+
3
+ setup(
4
+ name="tfcost",
5
+ version="0.1.0",
6
+ packages=find_packages(),
7
+ install_requires=[
8
+ "boto3"
9
+ ],
10
+ entry_points={
11
+ "console_scripts": [
12
+ "tfcost=tfcost.cli:main"
13
+ ]
14
+ },
15
+ author="Rakshith Jakkani",
16
+ description="Terraform cost estimation tool",
17
+ )
File without changes
@@ -0,0 +1,148 @@
1
+ import argparse
2
+ import sys
3
+
4
+ from tfcost.plan_parser import parse_plan
5
+ from tfcost.cost_calculator import calculate_cost
6
+ from tfcost.history import show_cost_trend
7
+
8
+
9
+ def main():
10
+
11
+ parser = argparse.ArgumentParser()
12
+
13
+ parser.add_argument(
14
+ "--summary",
15
+ action="store_true",
16
+ help="Show summary only"
17
+ )
18
+
19
+ parser.add_argument(
20
+ "--fail-if-increase",
21
+ type=float,
22
+ help="Fail if cost increase exceeds this value"
23
+ )
24
+
25
+ parser.add_argument(
26
+ "--output",
27
+ choices=["table", "json"],
28
+ default="table",
29
+ help="Output format"
30
+ )
31
+
32
+ parser.add_argument(
33
+ "--trend",
34
+ action="store_true",
35
+ help="Show infrastructure cost trend"
36
+ )
37
+
38
+ parser.add_argument(
39
+ "--var-file",
40
+ help="Terraform variable file (e.g. prod.tfvars)"
41
+ )
42
+
43
+ parser.add_argument(
44
+ "--compare",
45
+ nargs=2,
46
+ metavar=("ENV1", "ENV2"),
47
+ help="Compare two Terraform variable files"
48
+ )
49
+
50
+ parser.add_argument(
51
+ "--diff",
52
+ action="store_true",
53
+ help="Show cost difference between current and proposed infrastructure"
54
+ )
55
+
56
+ args = parser.parse_args()
57
+
58
+ # --------------------------------
59
+ # Cost trend
60
+ # --------------------------------
61
+ if args.trend:
62
+ show_cost_trend()
63
+ return
64
+
65
+ # --------------------------------
66
+ # Environment comparison
67
+ # --------------------------------
68
+ if args.compare:
69
+
70
+ env1, env2 = args.compare
71
+
72
+ print("\nRunning Terraform plan for:", env1)
73
+ resources1, region1 = parse_plan(var_file=env1)
74
+ cost1 = calculate_cost(resources1, region1, summary=True)
75
+
76
+ print("\nRunning Terraform plan for:", env2)
77
+ resources2, region2 = parse_plan(var_file=env2)
78
+ cost2 = calculate_cost(resources2, region2, summary=True)
79
+
80
+ diff = cost2 - cost1
81
+ percent = (diff / cost1 * 100) if cost1 else 0
82
+
83
+ print("\nTerraform Environment Cost Comparison")
84
+ print("─────────────────────────────────────")
85
+ print(f"{env1:20} ${cost1:>10.2f}")
86
+ print(f"{env2:20} ${cost2:>10.2f}")
87
+ print("")
88
+ print(f"{'Difference':20} ${diff:>10.2f}")
89
+ print(f"{'Change %':20} {percent:>10.2f} %")
90
+
91
+ return
92
+
93
+ # --------------------------------
94
+ # Pull Request Cost Diff
95
+ # --------------------------------
96
+ if args.diff:
97
+
98
+ print("\nCalculating current infrastructure cost...")
99
+ resources_old, region_old = parse_plan()
100
+ current_cost = calculate_cost(resources_old, region_old, summary=True)
101
+
102
+ print("\nCalculating proposed infrastructure cost...")
103
+ resources_new, region_new = parse_plan(var_file=args.var_file)
104
+ new_cost = calculate_cost(resources_new, region_new, summary=True)
105
+
106
+ diff = new_cost - current_cost
107
+ percent = (diff / current_cost * 100) if current_cost else 0
108
+
109
+ print("\nTerraform Cost Diff")
110
+ print("────────────────────────────────")
111
+ print(f"{'Current Infrastructure Cost':35} ${current_cost:>10.2f}")
112
+ print(f"{'Proposed Infrastructure Cost':35} ${new_cost:>10.2f}")
113
+ print("")
114
+ print(f"{'Cost Increase':35} ${diff:>10.2f}")
115
+ print(f"{'Change %':35} {percent:>10.2f}%")
116
+
117
+ if diff > 0:
118
+ print("\n⚠ Infrastructure cost will increase.")
119
+
120
+ return
121
+
122
+ # --------------------------------
123
+ # Normal cost estimation
124
+ # --------------------------------
125
+ resources, region = parse_plan(var_file=args.var_file)
126
+
127
+ cost_change = calculate_cost(
128
+ resources,
129
+ region,
130
+ summary=args.summary,
131
+ output=args.output
132
+ )
133
+
134
+ # --------------------------------
135
+ # Cost threshold guardrail
136
+ # --------------------------------
137
+ if args.fail_if_increase is not None:
138
+
139
+ threshold = args.fail_if_increase
140
+
141
+ print(f"\nThreshold : ${threshold:.2f}")
142
+
143
+ if cost_change > threshold:
144
+ print("Status : FAIL")
145
+ print("Cost increase exceeds allowed threshold.")
146
+ sys.exit(1)
147
+ else:
148
+ print("Status : PASS")
@@ -0,0 +1,170 @@
1
+ import json
2
+
3
+ from tfcost.engines.pricing_engine import calculate_resource_cost
4
+ from tfcost.pricing.service_detector import detect_service
5
+ from tfcost.history import save_cost_snapshot
6
+
7
+
8
+ def get_module_name(address):
9
+
10
+ if address.startswith("module."):
11
+ parts = address.split(".")
12
+ return parts[1]
13
+
14
+ return "root"
15
+
16
+
17
+ def calculate_cost(resources, region, summary=False, output="table"):
18
+
19
+ has_changes = False
20
+
21
+ show_resources = (not summary) and (output != "json")
22
+
23
+ previous_cost = 0
24
+ final_cost = 0
25
+
26
+ service_costs = {}
27
+ module_costs = {}
28
+
29
+ # Header will be printed ONLY if changes exist
30
+ printed_header = False
31
+
32
+ for r in resources:
33
+
34
+ actions = r.get("actions", [])
35
+
36
+ before_config = r.get("before")
37
+ after_config = r.get("config")
38
+
39
+ before_cost = 0
40
+ after_cost = 0
41
+
42
+ # -----------------------------
43
+ # BEFORE COST
44
+ # -----------------------------
45
+ if before_config:
46
+ before_resource = {
47
+ "type": r["type"],
48
+ "config": before_config
49
+ }
50
+
51
+ before_result = calculate_resource_cost(before_resource, region)
52
+ before_cost = before_result["cost"]
53
+
54
+ # -----------------------------
55
+ # AFTER COST
56
+ # -----------------------------
57
+ if after_config:
58
+ after_result = calculate_resource_cost(r, region)
59
+ after_cost = after_result["cost"]
60
+ service = after_result["service"]
61
+ else:
62
+ service = detect_service(r["type"])
63
+
64
+ diff = after_cost - before_cost
65
+
66
+ if diff != 0:
67
+ has_changes = True
68
+
69
+ # -----------------------------
70
+ # PRINT ONLY CHANGED RESOURCES
71
+ # -----------------------------
72
+ if show_resources and diff != 0:
73
+
74
+ # Print header lazily (only when first change appears)
75
+ if not printed_header:
76
+ print("\nTerraform Cost Estimate")
77
+ print("─" * 80)
78
+ print(f"{'Resource':35} {'Before':>10} {'After':>10} {'Diff':>10}")
79
+ print("─" * 80)
80
+ printed_header = True
81
+
82
+ if "create" in actions:
83
+ symbol = "+"
84
+ elif "delete" in actions:
85
+ symbol = "-"
86
+ elif "update" in actions:
87
+ symbol = "~"
88
+ else:
89
+ symbol = " "
90
+
91
+ print(
92
+ f"{symbol} {r['address']:33} "
93
+ f"${before_cost:>8.2f} → ${after_cost:>8.2f} "
94
+ f"{diff:+10.2f}"
95
+ )
96
+
97
+ # -----------------------------
98
+ # AGGREGATION
99
+ # -----------------------------
100
+ previous_cost += before_cost
101
+ final_cost += after_cost
102
+
103
+ service_costs[service] = service_costs.get(service, 0) + after_cost
104
+
105
+ module = get_module_name(r["address"])
106
+ module_costs[module] = module_costs.get(module, 0) + after_cost
107
+
108
+ cost_change = final_cost - previous_cost
109
+
110
+ result = {
111
+ "previous_cost": round(previous_cost, 2),
112
+ "estimated_cost": round(final_cost, 2),
113
+ "cost_change": round(cost_change, 2),
114
+ "services": {k: round(v, 2) for k, v in service_costs.items()},
115
+ "modules": {k: round(v, 2) for k, v in module_costs.items()}
116
+ }
117
+
118
+ # -----------------------------
119
+ # JSON OUTPUT
120
+ # -----------------------------
121
+ if output == "json":
122
+ print(json.dumps(result, indent=2))
123
+ return final_cost
124
+
125
+ # -----------------------------
126
+ # NO CHANGE CASE
127
+ # -----------------------------
128
+ if not has_changes:
129
+ print("\nNo cost changes detected. Infrastructure cost is up-to-date.")
130
+ save_cost_snapshot(final_cost)
131
+ return final_cost
132
+
133
+ # -----------------------------
134
+ # SUMMARY
135
+ # -----------------------------
136
+ print("─" * 80)
137
+ print(f"{'Previous Infrastructure Cost':45} ${previous_cost:>10.2f}")
138
+ print(f"{'Estimated Infrastructure Cost':45} ${final_cost:>10.2f}")
139
+ print(f"{'Cost Change':45} {cost_change:+10.2f}")
140
+ print("─" * 80)
141
+
142
+ # -----------------------------
143
+ # SERVICE BREAKDOWN
144
+ # -----------------------------
145
+ print("\nCost Breakdown by Service")
146
+ print("─" * 30)
147
+
148
+ for service, cost in service_costs.items():
149
+ if cost > 0:
150
+ print(f"{service:20} ${cost:>8.2f}")
151
+
152
+ print("─" * 30)
153
+
154
+ # -----------------------------
155
+ # MODULE BREAKDOWN
156
+ # -----------------------------
157
+ print("\nCost Breakdown by Module")
158
+ print("─" * 30)
159
+
160
+ for module, cost in module_costs.items():
161
+ print(f"{module:20} ${cost:>8.2f}")
162
+
163
+ print("─" * 30)
164
+
165
+ # -----------------------------
166
+ # SAVE HISTORY
167
+ # -----------------------------
168
+ save_cost_snapshot(final_cost)
169
+
170
+ return final_cost
@@ -0,0 +1,42 @@
1
+ import json
2
+ import os
3
+ from datetime import date
4
+
5
+ HISTORY_FILE = "cost_history.json"
6
+
7
+
8
+ def save_cost_snapshot(cost):
9
+
10
+ history = []
11
+
12
+ if os.path.exists(HISTORY_FILE):
13
+ with open(HISTORY_FILE) as f:
14
+ history = json.load(f)
15
+
16
+ # prevent duplicate entries
17
+ if history and history[-1]["cost"] == round(cost, 2):
18
+ return
19
+
20
+ history.append({
21
+ "date": str(date.today()),
22
+ "cost": round(cost, 2)
23
+ })
24
+
25
+ with open(HISTORY_FILE, "w") as f:
26
+ json.dump(history, f, indent=2)
27
+
28
+
29
+ def show_cost_trend():
30
+
31
+ if not os.path.exists(HISTORY_FILE):
32
+ print("\nNo cost history available.")
33
+ return
34
+
35
+ with open(HISTORY_FILE) as f:
36
+ history = json.load(f)
37
+
38
+ print("\nInfrastructure Cost Trend")
39
+ print("─" * 30)
40
+
41
+ for entry in history:
42
+ print(f"{entry['date']:12} ${entry['cost']:>8.2f}")
@@ -0,0 +1,77 @@
1
+ import json
2
+ import subprocess
3
+
4
+
5
+ def load_terraform_plan(var_file=None):
6
+
7
+ try:
8
+
9
+ # Build terraform plan command
10
+ cmd = ["terraform", "plan", "-out", "tfplan"]
11
+
12
+ if var_file:
13
+ cmd.append(f"-var-file={var_file}")
14
+ print("Running terraform plan...")
15
+ # Run terraform plan
16
+ subprocess.run(
17
+ cmd,
18
+ check=True,
19
+ capture_output=True,
20
+ text=True
21
+ )
22
+
23
+ # Get terraform JSON output
24
+ result = subprocess.run(
25
+ ["terraform", "show", "-json", "tfplan"],
26
+ check=True,
27
+ capture_output=True,
28
+ text=True
29
+ )
30
+
31
+ return json.loads(result.stdout)
32
+
33
+ except subprocess.CalledProcessError as e:
34
+ raise Exception("Terraform execution failed:\n" + e.stderr)
35
+
36
+
37
+ def parse_plan(var_file=None):
38
+
39
+ # Load Terraform plan
40
+ plan = load_terraform_plan(var_file)
41
+
42
+ resources = []
43
+ region = None
44
+
45
+ for r in plan.get("resource_changes", []):
46
+
47
+ change = r.get("change", {})
48
+ actions = change.get("actions", [])
49
+
50
+ before = change.get("before")
51
+ after = change.get("after")
52
+
53
+ # Use after for create/update, before for delete
54
+ config = after if after is not None else before
55
+
56
+ if config is None:
57
+ continue
58
+
59
+ # Detect region
60
+ if not region and isinstance(config, dict) and "region" in config:
61
+ region = config["region"]
62
+
63
+ resource = {
64
+ "type": r.get("type"),
65
+ "address": r.get("address"),
66
+ "config": after,
67
+ "before": before,
68
+ "actions": actions
69
+ }
70
+
71
+ resources.append(resource)
72
+
73
+ # Fallback region
74
+ if not region:
75
+ region = "us-east-1"
76
+
77
+ return resources, region
@@ -0,0 +1 @@
1
+ # pricing package
@@ -0,0 +1,17 @@
1
+ # from tfcost.engines.resource_engine import register
2
+ # from tfcost.pricing.aws_pricing import get_ebs_price
3
+
4
+ # @register("aws_ebs_volume")
5
+ # def price_ebs(resource, region):
6
+
7
+ # size = resource["config"].get("size")
8
+ # vtype = resource["config"].get("type", "gp3")
9
+
10
+ # price = get_ebs_price(vtype, region)
11
+
12
+ # monthly = price * size
13
+
14
+ # return {
15
+ # "service": "EBS",
16
+ # "cost": monthly
17
+ # }
@@ -0,0 +1,18 @@
1
+ # from tfcost.engines.resource_engine import register
2
+ # from tfcost.pricing.aws_pricing import get_ec2_price
3
+
4
+ # HOURS_PER_MONTH = 730.5
5
+
6
+ # @register("aws_instance")
7
+ # def price_ec2(resource, region):
8
+
9
+ # instance_type = resource["config"]["instance_type"]
10
+
11
+ # hourly = get_ec2_price(instance_type, region)
12
+
13
+ # monthly = hourly * HOURS_PER_MONTH
14
+
15
+ # return {
16
+ # "service": "EC2",
17
+ # "cost": monthly
18
+ # }
@@ -0,0 +1,15 @@
1
+ # from tfcost.engines.resource_engine import register
2
+
3
+ # HOURS_PER_MONTH = 730.5
4
+ # NAT_HOURLY_PRICE = 0.045
5
+
6
+
7
+ # @register("aws_nat_gateway")
8
+ # def price_nat_gateway(resource, region):
9
+
10
+ # monthly = NAT_HOURLY_PRICE * HOURS_PER_MONTH
11
+
12
+ # return {
13
+ # "service": "NAT Gateway",
14
+ # "cost": monthly
15
+ # }
@@ -0,0 +1,130 @@
1
+
2
+
3
+ import boto3
4
+ import json
5
+
6
+ pricing_client = boto3.client("pricing", region_name="us-east-1")
7
+
8
+ PRICE_CACHE = {}
9
+
10
+ def get_pricing_location(region):
11
+
12
+ REGION_MAP = {
13
+ "us-east-1": "US East (N. Virginia)",
14
+ "us-east-2": "US East (Ohio)",
15
+ "us-west-1": "US West (N. California)",
16
+ "us-west-2": "US West (Oregon)",
17
+ "eu-west-1": "EU (Ireland)",
18
+ "eu-west-2": "EU (London)",
19
+ "eu-west-3": "EU (Paris)",
20
+ "ap-south-1": "Asia Pacific (Mumbai)"
21
+ }
22
+
23
+ return REGION_MAP.get(region, "US East (N. Virginia)")
24
+
25
+ EC2_CLIENT_CACHE = {}
26
+
27
+ def get_ami_root_volume(ami_id, region):
28
+ if region not in EC2_CLIENT_CACHE:
29
+ EC2_CLIENT_CACHE[region] = boto3.client("ec2", region_name=region)
30
+ ec2 = EC2_CLIENT_CACHE[region]
31
+ response = ec2.describe_images(ImageIds=[ami_id])
32
+ images = response.get("Images", [])
33
+ if not images:
34
+ return None, None
35
+ image = images[0]
36
+ mappings = image.get("BlockDeviceMappings", [])
37
+ for mapping in mappings:
38
+ ebs = mapping.get("Ebs")
39
+ if ebs:
40
+ size = ebs.get("VolumeSize")
41
+ vtype = ebs.get("VolumeType")
42
+ return size, vtype
43
+
44
+ return None, None
45
+
46
+ def get_ec2_price(instance_type, region):
47
+
48
+ cache_key = f"{instance_type}:{region}"
49
+
50
+ if cache_key in PRICE_CACHE:
51
+ return PRICE_CACHE[cache_key]
52
+
53
+ # location = REGION_MAP.get(region, "US East (N. Virginia)")
54
+ location = get_pricing_location(region)
55
+
56
+ response = pricing_client.get_products(
57
+ ServiceCode="AmazonEC2",
58
+ Filters=[
59
+ {"Type": "TERM_MATCH", "Field": "instanceType", "Value": instance_type},
60
+ {"Type": "TERM_MATCH", "Field": "location", "Value": location},
61
+ {"Type": "TERM_MATCH", "Field": "operatingSystem", "Value": "Linux"},
62
+ {"Type": "TERM_MATCH", "Field": "tenancy", "Value": "Shared"},
63
+ {"Type": "TERM_MATCH", "Field": "preInstalledSw", "Value": "NA"},
64
+ {"Type": "TERM_MATCH", "Field": "capacitystatus", "Value": "Used"},
65
+ ],
66
+ MaxResults=1
67
+ )
68
+
69
+ if not response["PriceList"]:
70
+ return 0
71
+
72
+ price_item = json.loads(response["PriceList"][0])
73
+
74
+ terms = price_item["terms"]["OnDemand"]
75
+
76
+ for term in terms.values():
77
+ for dim in term["priceDimensions"].values():
78
+ price = float(dim["pricePerUnit"]["USD"])
79
+
80
+ # store in cache
81
+ PRICE_CACHE[cache_key] = price
82
+
83
+ return price
84
+
85
+ return 0
86
+
87
+
88
+ def get_ebs_price(volume_type, region):
89
+
90
+ # location = REGION_MAP.get(region, "US East (N. Virginia)")
91
+ location = get_pricing_location(region)
92
+
93
+ response = pricing_client.get_products(
94
+ ServiceCode="AmazonEC2",
95
+ Filters=[
96
+ {"Type": "TERM_MATCH", "Field": "volumeApiName", "Value": volume_type},
97
+ {"Type": "TERM_MATCH", "Field": "location", "Value": location},
98
+ {"Type": "TERM_MATCH", "Field": "productFamily", "Value": "Storage"}
99
+ ],
100
+ MaxResults=1
101
+ )
102
+
103
+ if not response["PriceList"]:
104
+ return 0
105
+
106
+ price_item = json.loads(response["PriceList"][0])
107
+
108
+ terms = price_item["terms"]["OnDemand"]
109
+
110
+ for term in terms.values():
111
+ for dim in term["priceDimensions"].values():
112
+ return float(dim["pricePerUnit"]["USD"])
113
+
114
+ return 0
115
+
116
+ def get_instance_price(instance_type, region):
117
+ """
118
+ Generic instance pricing.
119
+ Currently uses EC2 pricing API.
120
+ Works for EC2 and similar instance-based services.
121
+ """
122
+ return get_ec2_price(instance_type, region)
123
+
124
+
125
+ def get_storage_price(volume_type, region):
126
+ """
127
+ Generic storage pricing.
128
+ Currently uses EBS pricing API.
129
+ """
130
+ return get_ebs_price(volume_type, region)
@@ -0,0 +1,14 @@
1
+ # from tfcost.engines.resource_engine import register
2
+
3
+ # @register("aws_s3_bucket")
4
+ # def price_s3(resource, region):
5
+
6
+ # storage_gb = 10
7
+ # price_per_gb = 0.023
8
+
9
+ # monthly = storage_gb * price_per_gb
10
+
11
+ # return {
12
+ # "service": "S3",
13
+ # "cost": monthly
14
+ # }
@@ -0,0 +1,205 @@
1
+ RESOURCE_PRICING_MAP = {
2
+
3
+ # ========================
4
+ # EC2
5
+ # ========================
6
+ "aws_instance": {
7
+ "service": "EC2",
8
+ "lookup": "instance_type",
9
+ "pricing_model": "instance_hour"
10
+ },
11
+
12
+ "aws_launch_template": {
13
+ "service": "EC2",
14
+ "lookup": "instance_type",
15
+ "pricing_model": "instance_hour"
16
+ },
17
+
18
+ "aws_autoscaling_group": {
19
+ "service": "EC2",
20
+ "lookup": "instance_type",
21
+ "pricing_model": "instance_hour"
22
+ },
23
+
24
+ "aws_spot_instance_request": {
25
+ "service": "EC2",
26
+ "lookup": "instance_type",
27
+ "pricing_model": "instance_hour"
28
+ },
29
+
30
+ # ========================
31
+ # EBS
32
+ # ========================
33
+ "aws_ebs_volume": {
34
+ "service": "EBS",
35
+ "lookup": "type",
36
+ "size": "size",
37
+ "pricing_model": "storage_gb_month"
38
+ },
39
+
40
+ "aws_ebs_snapshot": {
41
+ "service": "EBS",
42
+ "size": "size",
43
+ "pricing_model": "storage_gb_month"
44
+ },
45
+
46
+ # ========================
47
+ # S3
48
+ # ========================
49
+ "aws_s3_bucket": {
50
+ "service": "S3",
51
+ "pricing_model": "bucket_storage",
52
+ "default_size": 10,
53
+ "price_per_gb": 0.023
54
+ },
55
+
56
+ # ========================
57
+ # RDS
58
+ # ========================
59
+ "aws_db_instance": {
60
+ "service": "RDS",
61
+ "lookup": "instance_class",
62
+ "pricing_model": "instance_hour"
63
+ },
64
+
65
+ "aws_rds_cluster_instance": {
66
+ "service": "RDS",
67
+ "lookup": "instance_class",
68
+ "pricing_model": "instance_hour"
69
+ },
70
+
71
+ "aws_rds_cluster": {
72
+ "service": "RDS",
73
+ "lookup": "engine",
74
+ "pricing_model": "instance_hour"
75
+ },
76
+
77
+ # ========================
78
+ # Load Balancers
79
+ # ========================
80
+ "aws_lb": {
81
+ "service": "ELB",
82
+ "pricing_model": "hourly_fixed",
83
+ "price": 0.025
84
+ },
85
+
86
+ "aws_elb": {
87
+ "service": "ELB",
88
+ "pricing_model": "hourly_fixed",
89
+ "price": 0.025
90
+ },
91
+
92
+ # ========================
93
+ # NAT Gateway
94
+ # ========================
95
+ "aws_nat_gateway": {
96
+ "service": "NAT Gateway",
97
+ "pricing_model": "hourly_fixed",
98
+ "price": 0.045
99
+ },
100
+
101
+ # ========================
102
+ # Elastic IP
103
+ # ========================
104
+ "aws_eip": {
105
+ "service": "Elastic IP",
106
+ "pricing_model": "hourly_fixed",
107
+ "price": 0.005
108
+ },
109
+
110
+ # ========================
111
+ # EFS
112
+ # ========================
113
+ "aws_efs_file_system": {
114
+ "service": "EFS",
115
+ "size": "size",
116
+ "pricing_model": "storage_gb_month"
117
+ },
118
+
119
+ # ========================
120
+ # DynamoDB
121
+ # ========================
122
+ "aws_dynamodb_table": {
123
+ "service": "DynamoDB",
124
+ "pricing_model": "capacity"
125
+ },
126
+
127
+ # ========================
128
+ # ElastiCache
129
+ # ========================
130
+ "aws_elasticache_cluster": {
131
+ "service": "ElastiCache",
132
+ "lookup": "node_type",
133
+ "pricing_model": "instance_hour"
134
+ },
135
+
136
+ "aws_elasticache_replication_group": {
137
+ "service": "ElastiCache",
138
+ "lookup": "node_type",
139
+ "pricing_model": "instance_hour"
140
+ },
141
+
142
+ # ========================
143
+ # Redshift
144
+ # ========================
145
+ "aws_redshift_cluster": {
146
+ "service": "Redshift",
147
+ "lookup": "node_type",
148
+ "pricing_model": "instance_hour"
149
+ },
150
+
151
+ # ========================
152
+ # EKS
153
+ # ========================
154
+ "aws_eks_cluster": {
155
+ "service": "EKS",
156
+ "pricing_model": "hourly_fixed",
157
+ "price": 0.10
158
+ },
159
+
160
+ "aws_eks_node_group": {
161
+ "service": "EKS",
162
+ "lookup": "instance_types",
163
+ "count": "desired_size",
164
+ "pricing_model": "eks_node_group"
165
+ },
166
+
167
+ # ========================
168
+ # MSK
169
+ # ========================
170
+ "aws_msk_cluster": {
171
+ "service": "MSK",
172
+ "lookup": "instance_type",
173
+ "pricing_model": "instance_hour"
174
+ },
175
+
176
+ # ========================
177
+ # OpenSearch
178
+ # ========================
179
+ "aws_opensearch_domain": {
180
+ "service": "OpenSearch",
181
+ "lookup": "instance_type",
182
+ "pricing_model": "instance_hour"
183
+ },
184
+
185
+ # ========================
186
+ # EKS
187
+ # ========================
188
+
189
+ "aws_eks_cluster": {
190
+ "service": "EKS",
191
+ "pricing_model": "hourly_fixed",
192
+ "price": 0.10
193
+ },
194
+
195
+ # ========================
196
+ # Glue
197
+ # ========================
198
+ "aws_glue_job": {
199
+ "service": "Glue",
200
+ "pricing_model": "hourly_fixed",
201
+ "price": 0.44
202
+ }
203
+
204
+
205
+ }
@@ -0,0 +1,33 @@
1
+ SERVICE_PREFIX_MAP = {
2
+
3
+ "aws_instance": "EC2",
4
+ "aws_ebs": "EBS",
5
+ "aws_s3": "S3",
6
+ "aws_lambda": "Lambda",
7
+ "aws_lb": "ELB",
8
+ "aws_elb": "ELB",
9
+ "aws_nat_gateway": "NAT Gateway",
10
+ "aws_dynamodb": "DynamoDB",
11
+ "aws_db": "RDS",
12
+ "aws_rds": "RDS",
13
+ "aws_redshift": "Redshift",
14
+ "aws_cloudfront": "CloudFront",
15
+ "aws_efs": "EFS",
16
+ "aws_elasticache": "ElastiCache",
17
+ "aws_kinesis": "Kinesis",
18
+ "aws_sqs": "SQS",
19
+ "aws_sns": "SNS",
20
+ "aws_api_gateway": "API Gateway",
21
+ "aws_eks": "EKS"
22
+ }
23
+
24
+
25
+ def detect_service(resource_type):
26
+
27
+ for prefix, service in SERVICE_PREFIX_MAP.items():
28
+
29
+ if resource_type.startswith(prefix):
30
+
31
+ return service
32
+
33
+ return "Other"
@@ -0,0 +1,13 @@
1
+ import subprocess
2
+
3
+ def run_terraform_plan(terraform_args):
4
+
5
+ cmd = ["terraform", "plan"] + list(terraform_args) + ["-out=tfplan"]
6
+
7
+ subprocess.run(cmd, check=True)
8
+
9
+ subprocess.run(
10
+ ["terraform", "show", "-json", "tfplan"],
11
+ stdout=open("plan.json", "w"),
12
+ check=True
13
+ )
@@ -0,0 +1,9 @@
1
+ Metadata-Version: 2.4
2
+ Name: tfcost
3
+ Version: 0.1.0
4
+ Summary: Terraform cost estimation tool
5
+ Author: Rakshith Jakkani
6
+ Requires-Dist: boto3
7
+ Dynamic: author
8
+ Dynamic: requires-dist
9
+ Dynamic: summary
@@ -0,0 +1,21 @@
1
+ setup.py
2
+ tfcost/__init__.py
3
+ tfcost/cli.py
4
+ tfcost/cost_calculator.py
5
+ tfcost/history.py
6
+ tfcost/plan_parser.py
7
+ tfcost/terraform_runner.py
8
+ tfcost.egg-info/PKG-INFO
9
+ tfcost.egg-info/SOURCES.txt
10
+ tfcost.egg-info/dependency_links.txt
11
+ tfcost.egg-info/entry_points.txt
12
+ tfcost.egg-info/requires.txt
13
+ tfcost.egg-info/top_level.txt
14
+ tfcost/pricing/__init__.py
15
+ tfcost/pricing/aws_ebs.py
16
+ tfcost/pricing/aws_ec2.py
17
+ tfcost/pricing/aws_nat_gateway.py
18
+ tfcost/pricing/aws_pricing.py
19
+ tfcost/pricing/aws_s3.py
20
+ tfcost/pricing/resource_pricing_map.py
21
+ tfcost/pricing/service_detector.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ tfcost = tfcost.cli:main
@@ -0,0 +1 @@
1
+ boto3
@@ -0,0 +1 @@
1
+ tfcost