Guides

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 estimate prices everything the workspace defines.
  • l4 diff prices the change against a baseline.

Both take --fail-above. Over the threshold they exit 2, and that exit code is the gate.

The two thresholds measure different things. On 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.29

Gating on the total

Exit 2 when the estimated monthly cost is above a threshold:

l4 estimate ./infra/ --fail-above 500 -q
echo $?
0

With a lower threshold:

l4 estimate ./infra/ --fail-above 1 -q
echo $?
Cost delta $52.29 exceeds threshold $1.00
2
Under the threshold the command exits 0, 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:

BaselineCommand
The git merge-base with main or master, the defaultl4 diff ./infra/
A specific git refl4 diff --base develop ./infra/
A snapshot saved by l4 estimate --out-filel4 diff baseline.json ./infra/
The default baseline needs git history. Both runners below check out a shallow clone by default, which leaves no merge-base to compare against, so the configurations set 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-comment

Wiring it into a pipeline

A job with no token exits 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/:

.github/workflows/cost-estimate.yml
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:

.github/workflows/cost-diff.yml
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 1

Two jobs in one file. The first gates the absolute cost, the second gates the delta.

.gitlab-ci.yml
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:

FlagDefaultDescription
--fail-above0Exit code 2 if the monthly cost (l4 estimate) or the monthly delta (l4 diff) exceeds the threshold
--max-resources500Maximum 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 estimate prices a workspace, with every flag and default
  • l4 diff does 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, --template and --csv
  • GitHub Actions posts cost reports through the API and SDKs rather than the CLI