CI/CD Integration
The LevelFour CLI (l4) prices a Terraform workspace inside a CI job, so a cost regression fails the pull request instead of arriving on next month's bill.
l4 estimateprices everything the workspace defines.l4 diffprices the change against a baseline.
Both take --fail-above. Over the threshold they exit 2, and that exit code is the gate.
l4 estimate the threshold applies to the absolute monthly cost of the whole workspace, so a repository that is already expensive trips it on every pull request. On l4 diff it applies only to the change.Store the API token as a CI secret and expose it as LEVELFOUR_TOKEN, as the configurations below do. A cost gate only reads, so a read key is enough. CLI authentication has the resolution order and the read versus read-write split.
Estimating a workspace
l4 estimate ./infra/Example output:
Name Quantity Unit Monthly Cost
aws_instance.bastion
├─ Instance usage (Linux, t4g.micro) 730 hours $6.13
└─ root_block_device
└─ Storage (General Purpose, gp3) 30 GB $2.40
aws_ecs_service.api
└─ Fargate vCPU 0.25 vCPU $7.39
aws_db_instance.this
└─ Instance usage (db.t4g.micro) 730 hours $12.41
Total $52.29Gating on the total
Exit 2 when the estimated monthly cost is above a threshold:
l4 estimate ./infra/ --fail-above 500 -q
echo $?0With a lower threshold:
l4 estimate ./infra/ --fail-above 1 -q
echo $?Cost delta $52.29 exceeds threshold $1.00
20, over it 2. The -q (quiet) flag suppresses the table output and communicates only via exit code, so a runner has one thing to read.Formatting for a pull request comment
--format github-comment emits a Markdown table you can pipe straight into a comment:
## Cost Estimate
| Module | Resource | Type | Cost/mo | Delta |
|--------|----------|------|---------|-------|
| | + bastion | aws_instance | $8.53 | +$8.53 |
| | + api | aws_ecs_service | $7.39 | +$7.39 |
| `module.db` | + this | aws_db_instance | $13.98 | +$13.98 |--out-file baseline.json writes the resource snapshot instead, for l4 diff to compare against later. l4 estimate covers both, along with the table and json formats.
Diffing a change
l4 diff reports the monthly delta between the workspace and a baseline. Pick the baseline:
| Baseline | Command |
|---|---|
The git merge-base with main or master, the default | l4 diff ./infra/ |
| A specific git ref | l4 diff --base develop ./infra/ |
A snapshot saved by l4 estimate --out-file | l4 diff baseline.json ./infra/ |
fetch-depth: 0 on GitHub Actions and GIT_DEPTH: 0 on GitLab CI. Where a runner has no git history at all, diff against a snapshot.The same flag gates the delta:
l4 diff ./infra/ --fail-above 100 --format github-commentWiring it into a pipeline
4, not 2. A gate that branches on any non-zero exit reports a cost breach when the real problem is an unset secret, so branch on 2. Exit codes has the full set.Each configuration downloads the release binary in the job. Installation covers Homebrew, go install, and pinning a version inside a CI image.
Comment the estimate on every pull request that touches infra/:
name: Cost Estimate
on:
pull_request:
paths:
- 'infra/**'
jobs:
cost-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install LevelFour CLI
run: |
curl -sSL https://github.com/LevelFourAI/levelfour-cli/releases/latest/download/l4_linux_amd64.tar.gz | tar xz
sudo mv l4 /usr/local/bin/
- name: Estimate costs
id: estimate
env:
LEVELFOUR_TOKEN: ${{ secrets.LEVELFOUR_TOKEN }}
run: |
l4 estimate ./infra/ --format github-comment > cost-estimate.md
- name: Comment on PR
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const body = fs.readFileSync('cost-estimate.md', 'utf8');
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: body,
});Gate the delta, and comment either way:
name: Cost Diff
on:
pull_request:
paths:
- 'infra/**'
jobs:
cost-diff:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install LevelFour CLI
run: |
curl -sSL https://github.com/LevelFourAI/levelfour-cli/releases/latest/download/l4_linux_amd64.tar.gz | tar xz
sudo mv l4 /usr/local/bin/
- name: Cost diff
id: diff
env:
LEVELFOUR_TOKEN: ${{ secrets.LEVELFOUR_TOKEN }}
run: |
l4 diff ./infra/ --format github-comment --fail-above 100 > cost-diff.md || echo "exit_code=$?" >> $GITHUB_OUTPUT
- name: Comment on PR
if: always()
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const body = fs.readFileSync('cost-diff.md', 'utf8');
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: body,
});
- name: Fail if over threshold
if: steps.diff.outputs.exit_code == '2'
run: exit 1Two jobs in one file. The first gates the absolute cost, the second gates the delta.
cost-estimate:
stage: validate
image: ubuntu:latest
before_script:
- curl -sSL https://github.com/LevelFourAI/levelfour-cli/releases/latest/download/l4_linux_amd64.tar.gz | tar xz
- mv l4 /usr/local/bin/
script:
- l4 estimate ./infra/ --fail-above 500 --format table
variables:
LEVELFOUR_TOKEN: $LEVELFOUR_TOKEN
rules:
- changes:
- infra/**
cost-diff:
stage: validate
image: ubuntu:latest
before_script:
- curl -sSL https://github.com/LevelFourAI/levelfour-cli/releases/latest/download/l4_linux_amd64.tar.gz | tar xz
- mv l4 /usr/local/bin/
script:
- l4 diff ./infra/ --fail-above 100 --format table
variables:
LEVELFOUR_TOKEN: $LEVELFOUR_TOKEN
GIT_DEPTH: 0
rules:
- changes:
- infra/**Flags
The defaults that matter in a pipeline:
| Flag | Default | Description |
|---|---|---|
--fail-above | 0 | Exit code 2 if the monthly cost (l4 estimate) or the monthly delta (l4 diff) exceeds the threshold |
--max-resources | 500 | Maximum number of resources to include |
l4 estimate and l4 diff carry the rest: pricing regions, Terraform variables, remote module downloads and output format.
Next
l4 estimateprices a workspace, with every flag and defaultl4 diffdoes the same for a change against a baseline- Exit codes are what a job should branch on
- Installation covers Homebrew,
go install, and pinning a version inside a CI image - CLI authentication has the token resolution order and the read versus read-write split
- Output formats covers
--json,--jq,--templateand--csv - GitHub Actions posts cost reports through the API and SDKs rather than the CLI
PR bot configuration
How the optional .levelfour/config.yml file tunes the PR bot per repository, where it lives, every field with its default, and what it does not control.
Integrate the SDK into your CLI
Embed the LevelFour SDK as a library inside an existing internal CLI to add cost-optimization subcommands.