-
Notifications
You must be signed in to change notification settings - Fork 1
187 lines (169 loc) · 7.19 KB
/
finops-cost-gate.yml
File metadata and controls
187 lines (169 loc) · 7.19 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
# ============================================================================
# FinOps Cost Gate Workflow
# ============================================================================
# This workflow estimates the monthly cloud cost of infrastructure changes
# using Infracost. It runs on pull requests that modify Terraform (.tf),
# Bicep (.bicep), or ARM template (.json) files.
#
# How it works:
# 1. Generates a cost baseline from the target branch.
# 2. Computes the cost diff introduced by the pull request.
# 3. Posts a cost summary comment on the PR.
# 4. Converts the cost report to SARIF and uploads it to the GitHub
# Security tab under the "finops-finding/" category.
# 5. Fails the workflow if estimated cost exceeds the monthly budget.
#
# Prerequisites:
# - Set the INFRACOST_API_KEY secret in your repository settings.
# - Optionally set the FINOPS_MONTHLY_BUDGET variable (defaults to $500).
#
# Lab references: Lab 09
# ============================================================================
# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
name: FinOps Cost Gate
on:
workflow_dispatch:
pull_request:
paths:
- '**/*.tf'
- '**/*.bicep'
- '**/*.json'
permissions:
contents: read
pull-requests: write
security-events: write
env:
MONTHLY_BUDGET: ${{ vars.FINOPS_MONTHLY_BUDGET || '500' }}
INFRACOST_API_KEY: ${{ secrets.INFRACOST_API_KEY }}
jobs:
cost-estimate:
name: FinOps — IaC Cost Estimation
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Infracost
uses: infracost/actions/setup@v3
with:
api-key: ${{ env.INFRACOST_API_KEY }}
- name: Generate Infracost baseline
if: github.event_name == 'pull_request'
run: |
git fetch origin ${{ github.event.pull_request.base.ref }} --depth=1
git checkout ${{ github.event.pull_request.base.ref }}
infracost breakdown --path . --format json --out-file /tmp/infracost-base.json || true
git checkout ${{ github.sha }}
- name: Generate Infracost diff
run: |
infracost diff \
--path . \
--compare-to /tmp/infracost-base.json \
--format json \
--out-file /tmp/infracost-diff.json || \
infracost breakdown \
--path . \
--format json \
--out-file /tmp/infracost-diff.json
- name: Post PR comment
if: github.event_name == 'pull_request'
run: |
infracost comment github \
--path /tmp/infracost-diff.json \
--repo ${{ github.repository }} \
--pull-request ${{ github.event.pull_request.number }} \
--github-token ${{ github.token }} \
--behavior update
- name: Convert cost report to SARIF
id: sarif
run: |
cat > convert-cost.js << 'SCRIPT'
const fs = require('fs');
const budget = parseInt(process.argv[2], 10);
const inputFile = process.argv[3];
const outputFile = process.argv[4];
let data;
try {
data = JSON.parse(fs.readFileSync(inputFile, 'utf8'));
} catch {
console.log('No Infracost output found, creating empty SARIF');
data = { totalMonthlyCost: '0', projects: [] };
}
const monthlyCost = parseFloat(data.totalMonthlyCost || '0');
const results = [];
const rules = [
{
id: 'budget-overspend',
shortDescription: { text: 'Estimated cost exceeds monthly budget' },
fullDescription: { text: `Estimated monthly cost exceeds the configured budget of $${budget}` },
help: { text: `Reduce resource costs to stay within the $${budget}/month budget`, markdown: `Reduce resource costs to stay within the **$${budget}/month** budget. Consider:\n- Smaller SKUs\n- Reserved instances\n- Spot instances\n- Removing unused resources` },
defaultConfiguration: { level: 'error' },
properties: { tags: ['finops', 'cost'] }
},
{
id: 'cost-increase',
shortDescription: { text: 'Resource cost increase detected' },
fullDescription: { text: 'A resource change increases estimated monthly cost' },
help: { text: 'Review the cost increase and confirm it is justified', markdown: 'Review the cost increase and confirm it is **justified** by the feature requirements.' },
defaultConfiguration: { level: 'warning' },
properties: { tags: ['finops', 'cost'] }
}
];
if (monthlyCost > budget) {
results.push({
ruleId: 'budget-overspend',
level: 'error',
message: { text: `Estimated monthly cost $${monthlyCost.toFixed(2)} exceeds budget $${budget}` },
locations: [{
physicalLocation: {
artifactLocation: { uri: '.' },
region: { startLine: 1 }
}
}],
partialFingerprints: { primaryLocationLineHash: `budget-${monthlyCost.toFixed(0)}` }
});
}
for (const project of (data.projects || [])) {
for (const diff of (project.diff?.resources || [])) {
if (parseFloat(diff.monthlyCost || '0') > 0) {
results.push({
ruleId: 'cost-increase',
level: 'warning',
message: { text: `${diff.name}: +$${parseFloat(diff.monthlyCost).toFixed(2)}/month` },
locations: [{
physicalLocation: {
artifactLocation: { uri: project.path || '.' },
region: { startLine: 1 }
}
}],
partialFingerprints: { primaryLocationLineHash: `cost-${diff.name}` }
});
}
}
}
const sarif = {
'$schema': 'https://raw.githubusercontent.com/oasis-tcs/sarif-spec/main/sarif-2.1/schema/sarif-schema-2.1.0.json',
version: '2.1.0',
runs: [{
tool: { driver: { name: 'finops-cost-gate', rules } },
results,
automationDetails: { id: 'finops-finding/' }
}]
};
fs.writeFileSync(outputFile, JSON.stringify(sarif, null, 2));
console.log(`Wrote ${results.length} findings to ${outputFile}`);
process.exit(monthlyCost > budget ? 1 : 0);
SCRIPT
node convert-cost.js "${{ env.MONTHLY_BUDGET }}" /tmp/infracost-diff.json finops-results.sarif
continue-on-error: true
- name: Upload FinOps SARIF
uses: github/codeql-action/upload-sarif@v4
if: always() && hashFiles('finops-results.sarif') != ''
with:
sarif_file: finops-results.sarif
category: finops-finding/
- name: Budget gate
run: |
if [ "${{ steps.sarif.outcome }}" = "failure" ]; then
echo "::error::Estimated monthly cost exceeds budget threshold of \$${{ env.MONTHLY_BUDGET }}"
exit 1
fi