Shift Cost Left: Adding Infracost to Your IaC Pipelines

I’ve talked before about the Cloud Management and Well-Architected Frameworks, and why it’s important to think carefully about how governance standards are introduced into our Infrastructure as Code (IaC) deployments. When governance is applied heavy-handedly it creates friction; when it’s automated and embedded into existing workflows, it becomes almost invisible. Over time, it simply becomes how things are done.

“By the time your FinOps team notices the problem, you’ve been running an oversized SQL database for three months. The infrastructure works perfectly, it’s just costing £2,000 more per month than it should. This is the cost of discovering problems after deployment.”

One of the key foundations of the Cloud Adoption Framework is Cost Management, and in this post I want to show you a simple, and free, way to build cost awareness directly into your pull requests and IaC pipelines, without slowing teams down or introducing additional gates.

Infracost

Infracost describes itself as a tool that helps teams “shift costs left”. In practical terms, this means making infrastructure costs visible before resources are deployed.

Traditionally, cost management is a shift-right activity: infrastructure is deployed first, and costs are analysed later through billing reports or cost management tools. By the time an issue is identified, the resources already exist and money has already been spent.

By integrating Infracost into your DevOps IaC pipelines, we can surface the cost impact of infrastructure changes much earlier, during code review and pull requests. This allows engineers to understand the financial implications of their changes at the point decisions are being made, rather than after the fact.

Infracost isn’t trying to replace your FinOps platform or cost management tools. Instead, it fills a specific gap: giving engineers cost visibility at the point they’re making infrastructure decisions, not weeks later when reviewing billing reports. Used correctly, it complements existing cost management tooling rather than replacing it.

Why This Matters for Platform Teams

For platform teams, cost management is rarely about the absolute cost of individual resources. It’s about predictability, guardrails, and enabling teams to make good decisions without constant oversight.

In most organisations, platform teams sit uncomfortably between:

  • engineering teams who want autonomy and speed
  • finance teams who want control, forecasting, and accountability

Traditional cost controls tend to sit too far to the right, budgets, alerts, and post-deployment reporting. While these are necessary, they’re inherently reactive. By the time a platform team is involved, the infrastructure already exists and the conversation becomes one of remediation rather than design.

Embedding Infracost into IaC pipelines changes that dynamic.

By surfacing cost information during pull requests, platform teams can:

  • make cost a design-time concern, not a retrospective one
  • encourage better architectural trade-offs without blocking deployments
  • standardise cost awareness across teams without enforcing rigid controls
  • reduce the need for manual reviews, exceptions, and escalations
  • avoid common mistakes such as over-sizing SKUs or selecting inappropriate service tiers

Crucially, this approach aligns with how platform teams should operate at scale. Instead of policing every deployment, the platform provides fast feedback and shared context, allowing product teams to self-correct early.

This is governance as a capability, not a checkpoint.

Infracost doesn’t replace budgets, cost alerts, or FinOps processes. It complements them by ensuring cost is considered before infrastructure decisions are locked in, when changes are cheap, conversations are easier, and autonomy can be preserved.

Here’s how to add Infracost to your pipelines.

Setting Up Infracost

Getting your API Key

The first step in the process is to setup a free account within Infracost. You can do this through their website, https://www.infracost.io/. You will just need your email address or to sign up with GitHub / Google. Once setup you need to get your free API Key.

Infracost works entirely from Terraform code and does not require access to your Azure subscription to calculate costs. The service hosts their own Pricing API (it works with multiple clouds – Azure, AWS, Google) and supports over 1100 Terraform resources. Note that not every single resource is visible yet but the list is always evolving.

To get your API key, sign in to your account, select Settings then Organisation Settings. Under the automation blade select API Tokens and copy the CLI and CI/CD token.

Getting the API Key From Infracost

Storing The Key

We need to securely store our API key somewhere that our pipelines can access. The simplest place is within a DevOps Pipeline Library.

Within your DevOps project, browse to Pipelines and select Library. Create a new Variable Group, we will call ours InfracostSecrets.

Storing the API Key in the Azure DevOps Library Variable Group
Azure DevOps Variable Library Group

Within the Library, create a new variable called INFRACOST_API_KEY and add the API key as the value, marking it secure by clicking the padlock icon.

You need to authorise your pipelines to be able to use the Library Variable Group by clicking pipeline permissions and adding your pipelines to the approved list.

Granting the Azure DevOps Pipeline access to the Variable Group
Pipeline Variables Approval

The Simple Terraform & Infracost Pipeline

Integrating Infracost into our existing pipelines is somewhat simple.

We first need to ensure our pipeline is using the Azure DevOps Variable Group from the library

variables:
  - group: InfracostSecrets

Then we need to install InfraCost to our DevOps Runners.

- bash: |
	curl -fsSL https://raw.githubusercontent.com/infracost/infracost/master/scripts/install.sh | sh
  	infracost --version
  displayName: Install Infracost

Then, we call Infracost (after the Terraform Init)

- bash: |
    infracost breakdown \
      --path . \
      --show-skipped \

   env:
     INFRACOST_API_KEY: $(INFRACOST_API_KEY)
   displayName: Infracost Breakdown

When we run the pipeline we can see a breakdown and summary table

The Infracost output summarised in a table and shown in the pipelines job
Infracost summary

The Pull Request Pipeline

Whilst our simple pipeline is effective, integrating Infracost into our pull request allows us to compare the current costs (from the main branch) to the feature/working branch of our pull request. We can also update the pull request with the cost summary which acts as an additional data point for pull request validation and approval.

The flow is a little more involved and the process diagram shows the steps involved.

A flow chart explaining the steps to integrate Infracost with a Pull Request Pipeline to compare costs
Pipeline Flow

As you can see, the pull request pipeline gets trigged when we create a pull request. When the pipeline runs it checks out the main branch (remember the PR Pipeline runs from the feature branch). Once Main is checked out we run infracost and store the detail in a temporary json. We then revert back to the pull request and run Infracost again. We then compare both the outputs of Infracost and post a nice comment to the pull request.

Infracost Pull Request Comment Added to the Azure DevOps Pull Request
Infracost Pull Request Comment

The breakdown shows each resource’s monthly cost, highlighting which components are fixed (like compute) versus usage-based (like storage). Resources marked ‘not supported yet’ won’t have cost estimates but are still tracked.

Using Infracost Results Effectively

  • Set informal thresholds (e.g., flag PRs that increase costs by >20%)
  • Use it to compare SKU options during design
  • Educate junior engineers on cost implications of architectural choices
  • Don’t block deployments based on cost alone—use it as a discussion point

Ensure your DevOps Build Service has the ability to contribute to pull requests via the Repo Security blade otherwise Infracost won’t be able to post the comment!

The Full Pull Request Pipeline

trigger: none

pr:
  branches:
    include:
      - main

pool:
  vmImage: "ubuntu-latest"

variables:
  # Fixed, deterministic inputs for PRs
  - name: environment
    value: prod
  - name: location
    value: centralus

  - name: management-subscription-id
    value: "your-management-sub-id"
  - name: TFSTATE_RESOURCE_GROUP
    value: "state-resource-group"
  - name: TFSTATE_STORAGE_ACCOUNT
    value: "state-storage-account"
  - name: TFSTATE_CONTAINER_NAME
    value: "tfstate"
  - group: InfracostSecrets

stages:
  - stage: ValidateAndPlan
    displayName: "Terraform PR Validate and Plan"
    jobs:
      - job: TerraformValidateAndPlan
        displayName: "Terraform Validate and Plan (PR)"
        steps:
          - checkout: self
            persistCredentials: true
            fetchDepth: 0 # fetch all history for accurate diffs

          # Generate TFSTATE_KEY
          - script: |
              key=$(echo "${SYSTEM_TEAMPROJECT}_$(environment)_$(location).tfstate" | tr '[:upper:]' '[:lower:]' | tr -d ' ')
              echo "Generated TFSTATE_KEY: $key"
              echo "##vso[task.setvariable variable=TFSTATE_KEY]$key"
            displayName: "Set dynamic TFSTATE_KEY"

          # Install Infracost
          - script: |
              curl -fsSL https://raw.githubusercontent.com/infracost/infracost/master/scripts/install.sh | sh
            displayName: "Install Infracost"

          # Save current PR commit
          - bash: |
              PR_COMMIT=$(git rev-parse HEAD)
              echo "Current PR commit: $PR_COMMIT"
              echo "##vso[task.setvariable variable=PR_COMMIT]$PR_COMMIT"
            displayName: "Save PR commit SHA"

          # Generate baseline from main branch
          - bash: |
              echo "Switching to main branch..."
              git fetch origin main
              git checkout origin/main

              echo "Initializing Terraform for main..."
              terraform init \
                -backend-config="resource_group_name=$(TFSTATE_RESOURCE_GROUP)" \
                -backend-config="storage_account_name=$(TFSTATE_STORAGE_ACCOUNT)" \
                -backend-config="container_name=$(TFSTATE_CONTAINER_NAME)" \
                -backend-config="key=$(TFSTATE_KEY)" \
                -input=false

              echo "Running Infracost baseline..."
              infracost breakdown \
                --path . \
                --format json \
                --out-file /tmp/infracost-base.json
            env:
              INFRACOST_API_KEY: $(INFRACOST_API_KEY)
              ARM_USE_OIDC: true
              ARM_CLIENT_ID: $(ARM_CLIENT_ID)
              ARM_TENANT_ID: $(ARM_TENANT_ID)
              ARM_SUBSCRIPTION_ID: $(management-subscription-id)
            displayName: "Infracost baseline (main)"

          # Switch back to PR branch
          - bash: |
              echo "Switching back to PR branch..."
              git checkout $(PR_COMMIT)
            displayName: "Switch back to PR branch"

          # Shared terraform setup & init for PR branch
          - template: templates/terraform-setup.yml
            parameters:
              tfstateKey: $(TFSTATE_KEY)

          # Generate Infracost diff for PR
          - bash: |
              echo "Running Infracost diff for PR..."
              infracost diff \
                --path . \
                --compare-to /tmp/infracost-base.json \
                --format json \
                --out-file /tmp/infracost-diff.json
            env:
              INFRACOST_API_KEY: $(INFRACOST_API_KEY)
              ARM_USE_OIDC: true
              ARM_CLIENT_ID: $(ARM_CLIENT_ID)
              ARM_TENANT_ID: $(ARM_TENANT_ID)
              ARM_SUBSCRIPTION_ID: $(management-subscription-id)
            displayName: "Infracost diff (PR vs main)"

          # Display Infracost diff in table format
          - bash: |
              echo "Displaying cost difference..."
              infracost output \
                --path /tmp/infracost-diff.json \
                --format diff
            env:
              INFRACOST_API_KEY: $(INFRACOST_API_KEY)
            displayName: "Display Infracost diff table"

          # Post comment to PR
          - bash: |
              infracost comment azure-repos \
                --path /tmp/infracost-diff.json \
                --azure-access-token $(System.AccessToken) \
                --pull-request $(System.PullRequest.PullRequestId) \
                --repo-url $(Build.Repository.Uri) \
                --behavior update
            env:
              INFRACOST_API_KEY: $(INFRACOST_API_KEY)
            displayName: "Post Infracost comment to PR"

Summary

Cost management is a critical capability that is too often pushed to the post-deployment phase, where decisions are harder to reverse and costs have already been incurred. While Infracost does not replace the need for ongoing cost management and FinOps practices, it does enable teams to make more informed, intentional decisions earlier in the delivery lifecycle, and to catch costly mistakes before they reach production.

By introducing Infracost into the pull request process, cost awareness becomes part of the platform itself rather than an external control. Governance is embedded into existing workflows, feedback is delivered at the right time, and teams retain autonomy while operating within clear, well-understood guardrails.

This is cost governance done the right way: early, automated, and aligned with how teams already work.

Your subscription could not be saved. Please try again.
Your subscription to deniscooper.co.uk has been successful.

Join Cloud Engineers Who Build Smarter on Azure

Free checklists, practical guides and the latest posts — built for cloud engineers working with Azure and Terraform.

1 thought on “Shift Cost Left: Adding Infracost to Your IaC Pipelines”

  1. Thank you so much for this breakdown of Infracost! It’s very important to keep an eye on cloud spend. Definitely going to give this a try on my next project.

    Reply

Leave a Comment