@aaa-sentinel/iac-cost 0.0.0-stage → 0.1.0
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.
- package/LICENSE +21 -0
- package/README.md +188 -3
- package/dist/cli.js +4522 -0
- package/package.json +41 -6
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 AAA Sentinel
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,188 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
# @aaa-sentinel/iac-cost
|
|
2
|
+
|
|
3
|
+
Comments the **monthly cost delta of a Terraform plan** on the pull request that introduces it,
|
|
4
|
+
so the price of a change is visible while it is still cheap to change.
|
|
5
|
+
|
|
6
|
+
> ### 💲 Infrastructure cost estimate
|
|
7
|
+
>
|
|
8
|
+
> **Coverage: 3 of 8 billable resources priced** (4 more have no cost by design).
|
|
9
|
+
>
|
|
10
|
+
> **Estimated change: +$74.46/mo** (list price, USD)
|
|
11
|
+
>
|
|
12
|
+
> | Resource | Change | Qty | Delta |
|
|
13
|
+
> | --- | --- | --- | --- |
|
|
14
|
+
> | `aws_instance` | new | 2 | +$121.18/mo |
|
|
15
|
+
> | `azurerm_linux_virtual_machine` | removed | 1 | -$46.72/mo |
|
|
16
|
+
>
|
|
17
|
+
> <details><summary>Not priced (5 resources)</summary></details>
|
|
18
|
+
|
|
19
|
+
## Requirements
|
|
20
|
+
|
|
21
|
+
- Node.js 20 or newer.
|
|
22
|
+
- An OAuth2 application with the `iac:estimate` scope, created in the developer portal.
|
|
23
|
+
That scope grants **nothing else** — it cannot read your accounts, findings or reports —
|
|
24
|
+
which is what makes it safe to paste into a CI that third parties can trigger.
|
|
25
|
+
- A **Pro** or **Business** plan.
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
terraform plan -out=plan.tfplan
|
|
31
|
+
terraform show -json plan.tfplan > plan.json
|
|
32
|
+
|
|
33
|
+
npx @aaa-sentinel/iac-cost plan.json # or: … | npx @aaa-sentinel/iac-cost
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
| Variable | Required | Meaning |
|
|
37
|
+
| --- | --- | --- |
|
|
38
|
+
| `IAC_COST_CLIENT_ID` | yes | Client ID of the OAuth2 application |
|
|
39
|
+
| `IAC_COST_CLIENT_SECRET` | yes | Client secret. Store it as an encrypted CI secret |
|
|
40
|
+
| `IAC_COST_BASE_URL` | no | Defaults to `https://api.aaa-sentinel.com`. Must be `https` — the secret is never sent over plain HTTP |
|
|
41
|
+
|
|
42
|
+
The Markdown comment goes to **stdout**; warnings go to **stderr**. If there is nothing to
|
|
43
|
+
say, stdout is empty.
|
|
44
|
+
|
|
45
|
+
### It never fails your build
|
|
46
|
+
|
|
47
|
+
The exit code is **always 0**. A missing credential, a network error, an unreadable plan or an
|
|
48
|
+
outage on our side prints a one-line warning and stops. A cost comment is not a quality gate, and
|
|
49
|
+
our availability must not become a dependency of your deployments.
|
|
50
|
+
|
|
51
|
+
## What actually leaves your machine
|
|
52
|
+
|
|
53
|
+
Not the plan. `terraform show -json` output carries CIDRs, account identifiers and, often,
|
|
54
|
+
secrets in clear text. Before anything is sent, the plan is reduced **on your machine** to a
|
|
55
|
+
whitelist: resource type, action (`create`/`update`/`delete`/`replace`), how many instances the
|
|
56
|
+
block produces, and the handful of attributes that move the price (instance size, region, OS).
|
|
57
|
+
Everything else is dropped before serialising, and the projection is never written to the CI log.
|
|
58
|
+
|
|
59
|
+
## Reading the number
|
|
60
|
+
|
|
61
|
+
- **List price.** Neither AWS, Azure nor Google returns what *you* pay. Enterprise agreements
|
|
62
|
+
(EDP/EA/CUD) and existing reservations or Savings Plans are invisible at pull request time —
|
|
63
|
+
a reservation that already covers the new resource makes the real marginal cost zero.
|
|
64
|
+
- **Priced in USD, shown in your organization's currency.** USD is what the providers publish, and
|
|
65
|
+
the API always returns it (`...Usd` fields). If your AAA Sentinel organization displays amounts
|
|
66
|
+
in EUR or GBP, the comment shows the converted amount instead, marked with `≈` and with the ECB
|
|
67
|
+
reference rate and its date spelled out — it is an estimate on top of an estimate, and the same
|
|
68
|
+
unchanged pull request can move a little as the exchange rate does. Organizations in USD, or a
|
|
69
|
+
day with no exchange rate available, get plain USD.
|
|
70
|
+
- **Coverage leads, not the total.** When fewer than half of the billable resources can be
|
|
71
|
+
priced, the breakdown is published but the total is withheld. A total built from 2 resources
|
|
72
|
+
out of 30 is not a partial estimate, it is noise — and it would end up quoted in the review as
|
|
73
|
+
if it were the figure.
|
|
74
|
+
- **An unknown region is treated as an unsupported one.** In AWS the region lives in the
|
|
75
|
+
`provider` block rather than the resource, so `terraform show -json` often leaves it unknown.
|
|
76
|
+
We say we do not know rather than substituting another region's price.
|
|
77
|
+
|
|
78
|
+
Currently priced: **AWS** (EC2 on demand) and **Azure** (virtual machines). Anything else is
|
|
79
|
+
reported as not priced, never as free.
|
|
80
|
+
|
|
81
|
+
## GitHub Actions
|
|
82
|
+
|
|
83
|
+
Posts one comment per pull request and **updates it in place** on later pushes instead of
|
|
84
|
+
stacking a new one each time. The token is the one the run already injects — no personal access
|
|
85
|
+
token, no app installation.
|
|
86
|
+
|
|
87
|
+
```yaml
|
|
88
|
+
name: Infrastructure cost
|
|
89
|
+
on: pull_request
|
|
90
|
+
|
|
91
|
+
permissions:
|
|
92
|
+
contents: read
|
|
93
|
+
pull-requests: write
|
|
94
|
+
|
|
95
|
+
jobs:
|
|
96
|
+
cost:
|
|
97
|
+
runs-on: ubuntu-latest
|
|
98
|
+
steps:
|
|
99
|
+
- uses: actions/checkout@v4
|
|
100
|
+
- uses: hashicorp/setup-terraform@v3
|
|
101
|
+
- uses: actions/setup-node@v4
|
|
102
|
+
with:
|
|
103
|
+
node-version: 20
|
|
104
|
+
|
|
105
|
+
- run: terraform init
|
|
106
|
+
- run: terraform plan -out=plan.tfplan
|
|
107
|
+
- run: terraform show -json plan.tfplan > plan.json
|
|
108
|
+
|
|
109
|
+
- name: Estimate cost
|
|
110
|
+
env:
|
|
111
|
+
IAC_COST_CLIENT_ID: ${{ secrets.IAC_COST_CLIENT_ID }}
|
|
112
|
+
IAC_COST_CLIENT_SECRET: ${{ secrets.IAC_COST_CLIENT_SECRET }}
|
|
113
|
+
run: npx --yes @aaa-sentinel/iac-cost plan.json > comment.md
|
|
114
|
+
|
|
115
|
+
- name: Comment on the pull request
|
|
116
|
+
continue-on-error: true
|
|
117
|
+
env:
|
|
118
|
+
GH_TOKEN: ${{ github.token }}
|
|
119
|
+
PR: ${{ github.event.number }}
|
|
120
|
+
run: |
|
|
121
|
+
[ -s comment.md ] || exit 0
|
|
122
|
+
id=$(gh api --paginate "repos/$GITHUB_REPOSITORY/issues/$PR/comments" \
|
|
123
|
+
--jq '.[] | select((.body // "") | contains("<!-- aaa-sentinel:iac-cost -->")) | .id' \
|
|
124
|
+
| head -n1)
|
|
125
|
+
if [ -n "$id" ]; then
|
|
126
|
+
gh api -X PATCH "repos/$GITHUB_REPOSITORY/issues/comments/$id" -F body=@comment.md
|
|
127
|
+
else
|
|
128
|
+
gh api -X POST "repos/$GITHUB_REPOSITORY/issues/$PR/comments" -F body=@comment.md
|
|
129
|
+
fi
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
> `continue-on-error` is not decoration: without it, a GitHub API hiccup while posting the comment
|
|
133
|
+
> would fail the job, and a cost comment must never be able to do that.
|
|
134
|
+
|
|
135
|
+
> On a pull request from a fork, GitHub gives the run a read-only token, so the comment step is
|
|
136
|
+
> skipped. That is a GitHub restriction and it applies to every tool that comments on pull
|
|
137
|
+
> requests, not just this one.
|
|
138
|
+
|
|
139
|
+
## Bitbucket Pipelines
|
|
140
|
+
|
|
141
|
+
Bitbucket does not inject a token that can write to the repository, so create a **Repository
|
|
142
|
+
Access Token** with the `pullrequest:write` scope and store it as a secured repository variable
|
|
143
|
+
named `BB_TOKEN`.
|
|
144
|
+
|
|
145
|
+
```yaml
|
|
146
|
+
pipelines:
|
|
147
|
+
pull-requests:
|
|
148
|
+
'**':
|
|
149
|
+
- step:
|
|
150
|
+
name: Infrastructure cost
|
|
151
|
+
image: hashicorp/terraform:latest
|
|
152
|
+
script:
|
|
153
|
+
- apk add --no-cache nodejs npm curl jq
|
|
154
|
+
- terraform init
|
|
155
|
+
- terraform plan -out=plan.tfplan
|
|
156
|
+
- terraform show -json plan.tfplan > plan.json
|
|
157
|
+
- npx --yes @aaa-sentinel/iac-cost plan.json > comment.md
|
|
158
|
+
- |
|
|
159
|
+
set +e
|
|
160
|
+
[ -s comment.md ] || exit 0
|
|
161
|
+
API="https://api.bitbucket.org/2.0/repositories/$BITBUCKET_WORKSPACE/$BITBUCKET_REPO_SLUG/pullrequests/$BITBUCKET_PR_ID/comments"
|
|
162
|
+
BODY=$(jq -Rs . < comment.md)
|
|
163
|
+
ID=$(curl -sf -H "Authorization: Bearer $BB_TOKEN" "$API?pagelen=100" \
|
|
164
|
+
| jq -r '[.values[] | select((.content.raw // "") | contains("<!-- aaa-sentinel:iac-cost -->"))][0].id // empty')
|
|
165
|
+
if [ -n "$ID" ]; then
|
|
166
|
+
curl -sf -X PUT -H "Authorization: Bearer $BB_TOKEN" -H 'Content-Type: application/json' \
|
|
167
|
+
-d "{\"content\":{\"raw\":$BODY}}" "$API/$ID"
|
|
168
|
+
else
|
|
169
|
+
curl -sf -X POST -H "Authorization: Bearer $BB_TOKEN" -H 'Content-Type: application/json' \
|
|
170
|
+
-d "{\"content\":{\"raw\":$BODY}}" "$API"
|
|
171
|
+
fi
|
|
172
|
+
exit 0
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
> The `set +e` and the final `exit 0` are there for the same reason as `continue-on-error` on
|
|
176
|
+
> GitHub: a failure while commenting must not fail the pipeline. A deleted comment has a null
|
|
177
|
+
> `content.raw`, hence the `// ""` — without it `jq` aborts and the comment is posted again
|
|
178
|
+
> instead of updated.
|
|
179
|
+
|
|
180
|
+
## Why there is no comment
|
|
181
|
+
|
|
182
|
+
| What you see | Why |
|
|
183
|
+
| --- | --- |
|
|
184
|
+
| No comment at all | The plan changes no billable resource, or the credentials are missing — check the step log, the reason is always printed |
|
|
185
|
+
| A breakdown with no total | Fewer than half of the billable resources could be priced |
|
|
186
|
+
| `region could not be resolved from the plan` | The region comes from a variable or an aliased provider, so the plan does not say which one applies |
|
|
187
|
+
| `provider recognized, pricing not connected yet` | A cloud we support but whose catalogue is not connected yet (Google) |
|
|
188
|
+
| `no matching price found in the catalog` | The size exists in the plan but not in that region's catalogue — an instance type not offered there, or a resource we price only for some families |
|