Building a Production-Grade CI/CD Pipeline — Part 3: Adding Cloud Cost Optimization

This tutorial extends a production-grade CI/CD pipeline by adding infrastructure cost visibility directly into the pull request workflow. Using Terraform, Infracost, GitHub Actions, and Slack alerts, the article demonstrates how to estimate cloud cost deltas, enforce budget thresholds, create override workflows, and turn cloud spending into a first-class engineering concern rather than a delayed finance problem.


This content originally appeared on HackerNoon and was authored by emmanuela Opurum

In Part 1, we built a pipeline that builds, tests, and deploys reliably. In Part 2, we added AI-powered security scanning that blocks bad code before it reaches staging. In this final part, we solve the last gap: cost visibility.

Right now, a developer can open a PR that changes an ECS task definition, a Terraform RDS instance type, or a CloudFront distribution config, and nobody knows the cost impact until the AWS invoice arrives three weeks later. We're going to fix that.

By the end of this article, your pipeline will estimate the cloud cost delta of every PR, post a clear cost report as a PR comment, and block deployment if the projected increase exceeds your team's budget threshold.

Why Cost Belongs in the Pipeline

This needs to be said plainly: cloud cost is an engineering problem, not a finance problem.

The teams that control their cloud spend are the ones where cost visibility is embedded in the developer workflow, not delegated to a FinOps function that publishes monthly reports. A monthly report tells you what happened. A pipeline cost gate changes what engineers decide to do in the first place.

The Flexera 2026 State of the Cloud Report found that organisations waste an average of 28% of their cloud spend. That's not a procurement failure. That's an architecture and tooling failure. Engineers make decisions every day that affect cloud cost, instance types, retention policies, replication factors, caching strategies, without any cost signal at the point of decision.

A CI/CD cost gate is that signal. Here's how to build one.

What We're Adding

PR opened with Terraform changes
        ↓
Infracost runs against the PR diff
        ↓
Cost delta calculated (current vs proposed)
        ↓
Report posted as PR comment (breakdown by resource)
        ↓
Budget gate: cost increase > threshold?
  YES → Block merge + Slack alert to engineering lead
  NO  → Pipeline continues to security scan + deploy

The Stack

  • Infracost — open source cloud cost estimation for Terraform
  • GitHub Actions — runs the cost check on every PR
  • Infracost Cloud (free tier) — stores historical cost data and trend analysis
  • Slack — alerts when the budget gate triggers

Step 1: Add Terraform to the Project

If you're not already using Terraform, here's a minimal configuration for the ECS service we deployed in Part 1:

hcl

# infra/main.tf
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
  backend "s3" {
    bucket = "your-terraform-state-bucket"
    key    = "cicd-demo/terraform.tfstate"
    region = "eu-west-1"
  }
}

provider "aws" {
  region = var.aws_region
}

# ECS Cluster
resource "aws_ecs_cluster" "main" {
  name = "${var.app_name}-${var.environment}"
  setting {
    name  = "containerInsights"
    value = "enabled"
  }
}

# ECS Task Definition
resource "aws_ecs_task_definition" "app" {
  family                   = "${var.app_name}-${var.environment}"
  network_mode             = "awsvpc"
  requires_compatibilities = ["FARGATE"]
  cpu                      = var.task_cpu
  memory                   = var.task_memory
  execution_role_arn       = aws_iam_role.ecs_execution.arn

  container_definitions = jsonencode([{
    name  = var.app_name
    image = "${var.ecr_repository_url}:latest"
    portMappings = [{ containerPort = 3000, protocol = "tcp" }]
    logConfiguration = {
      logDriver = "awslogs"
      options = {
        awslogs-group         = "/ecs/${var.app_name}-${var.environment}"
        awslogs-region        = var.aws_region
        awslogs-stream-prefix = "ecs"
      }
    }
  }])
}

# ECS Service
resource "aws_ecs_service" "app" {
  name            = "${var.app_name}-api"
  cluster         = aws_ecs_cluster.main.id
  task_definition = aws_ecs_task_definition.app.arn
  desired_count   = var.desired_count
  launch_type     = "FARGATE"

  network_configuration {
    subnets          = var.private_subnet_ids
    security_groups  = [aws_security_group.ecs_tasks.id]
    assign_public_ip = false
  }

  load_balancer {
    target_group_arn = aws_lb_target_group.app.arn
    container_name   = var.app_name
    container_port   = 3000
  }
}

# RDS Instance (PostgreSQL)
resource "aws_db_instance" "main" {
  identifier           = "${var.app_name}-${var.environment}"
  engine               = "postgres"
  engine_version       = "16.1"
  instance_class       = var.db_instance_class
  allocated_storage    = var.db_storage_gb
  storage_encrypted    = true
  deletion_protection  = var.environment == "production" ? true : false
  skip_final_snapshot  = var.environment != "production"

  db_name  = var.app_name
  username = var.db_username
  password = var.db_password

  vpc_security_group_ids = [aws_security_group.rds.id]
  db_subnet_group_name   = aws_db_subnet_group.main.name

  backup_retention_period = var.environment == "production" ? 7 : 1

  tags = {
    Environment = var.environment
    ManagedBy   = "terraform"
  }
}

\

# infra/variables.tf
variable "app_name"          { default = "cicd-demo" }
variable "environment"       { default = "staging" }
variable "aws_region"        { default = "eu-west-1" }
variable "task_cpu"          { default = 256 }
variable "task_memory"       { default = 512 }
variable "desired_count"     { default = 1 }
variable "db_instance_class" { default = "db.t3.micro" }
variable "db_storage_gb"     { default = 20 }
variable "ecr_repository_url" {}
variable "private_subnet_ids" { type = list(string) }
variable "db_username"        {}
variable "db_password"        { sensitive = true }

Step 2: Install Infracost and Get Your API Key

Sign up at infracost.io, the free tier is sufficient for this setup.

# Install locally for testing
brew install infracost          # macOS
# or
curl -fsSL https://raw.githubusercontent.com/infracost/infracost/master/scripts/install.sh | sh

# Authenticate
infracost auth login

# Test locally against your Terraform directory
infracost breakdown --path infra/

You'll see output like this:

Project: cicd-demo

 Name                                             Monthly Cost

 aws_ecs_service.app
 └─ Per GB per hour (Linux/UNIX, Fargate)         $9.55

 aws_db_instance.main
 └─ Database instance (db.t3.micro, 730 hrs)      $15.33
 └─ Storage (gp2, 20 GB)                          $2.30

 OVERALL TOTAL                                    $27.18

Copy your API key from the Infracost dashboard and add it to GitHub Secrets as INFRACOST_API_KEY.

Step 3: Add the Cost Gate Workflow

Create a new workflow specifically for cost checking:

# .github/workflows/cost-gate.yml
name: Cloud Cost Gate

on:
  pull_request:
    paths:
      - 'infra/**'
      - '*.tf'
      - '**/*.tf'

jobs:
  cost-check:
    name: Cloud Cost Estimation
    runs-on: ubuntu-latest
    permissions:
      pull-requests: write
      contents: read

    steps:
      - name: Checkout PR branch
        uses: actions/checkout@v4

      - name: Setup Infracost
        uses: infracost/actions/setup@v3
        with:
          api-key: ${{ secrets.INFRACOST_API_KEY }}

      - name: Generate Infracost diff
        run: |
          # Estimate cost of current main branch (baseline)
          git fetch origin main
          git checkout origin/main -- infra/

          infracost breakdown \
            --path infra/ \
            --format json \
            --out-file /tmp/infracost-base.json

          # Restore PR branch infra
          git checkout HEAD -- infra/

          # Generate diff between base and PR
          infracost diff \
            --path infra/ \
            --compare-to /tmp/infracost-base.json \
            --format json \
            --out-file /tmp/infracost-diff.json

      - name: Parse cost delta and apply budget gate
        id: cost-gate
        env:
          BUDGET_THRESHOLD: ${{ vars.MONTHLY_BUDGET_THRESHOLD || '50' }}
        run: |
          node - <<'EOF'
          const fs = require('fs');

          const diff = JSON.parse(fs.readFileSync('/tmp/infracost-diff.json', 'utf8'));
          const totalDiff = parseFloat(diff.diffTotalMonthlyCost || 0);
          const threshold = parseFloat(process.env.BUDGET_THRESHOLD);

          console.log(`Cost delta: $${totalDiff.toFixed(2)}/month`);
          console.log(`Budget threshold: $${threshold}/month`);

          const blocked = totalDiff > threshold;

          fs.appendFileSync(process.env.GITHUB_OUTPUT, 
            `cost_delta=${totalDiff.toFixed(2)}\n`);
          fs.appendFileSync(process.env.GITHUB_OUTPUT,
            `blocked=${blocked}\n`);
          fs.appendFileSync(process.env.GITHUB_OUTPUT,
            `threshold=${threshold}\n`);

          if (blocked) {
            console.log(`GATE TRIGGERED: $${totalDiff.toFixed(2)} exceeds $${threshold} threshold`);
          }
          EOF

      - name: Post cost report as PR comment
        uses: infracost/actions/comment@v3
        with:
          path: /tmp/infracost-diff.json
          behavior: update
          tag: infracost-cost-gate

      - name: Post budget gate status to PR
        uses: actions/github-script@v7
        with:
          script: |
            const delta = '${{ steps.cost-gate.outputs.cost_delta }}';
            const blocked = '${{ steps.cost-gate.outputs.blocked }}' === 'true';
            const threshold = '${{ steps.cost-gate.outputs.threshold }}';

            const icon = blocked ? ' ' : '';
            const status = blocked ? 'BUDGET GATE TRIGGERED' : 'Within budget threshold';
            const colour = blocked ? '> ' : '> ';

            const body = `## ${icon} Cloud Cost Gate: ${status}

            ${colour} **Monthly cost delta: $${delta}** (threshold: $${threshold}/month)

            ${blocked 
              ? `This PR would increase monthly cloud spend by **$${delta}**, which exceeds the configured threshold of **$${threshold}/month**.\n\nThis PR is blocked from merging until a budget owner reviews and approves. Please:\n1. Ping \`@platform-team\` in Slack for a cost review\n2. Consider whether this infrastructure change can be right-sized\n3. If approved, a team lead can override by adding the \`cost-approved\` label`
              : `This PR is within the configured budget threshold. Cost changes are acceptable.`}

            *Powered by Infracost + GitHub Actions*`;

            await github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body
            });

      - name: Alert Slack if budget gate triggered
        if: steps.cost-gate.outputs.blocked == 'true'
        uses: slackapi/slack-github-action@v1
        with:
          channel-id: ${{ secrets.SLACK_CHANNEL_ID }}
          payload: |
            {
              "blocks": [
                {
                  "type": "header",
                  "text": { "type": "plain_text", "text": " Budget Gate Triggered" }
                },
                {
                  "type": "section",
                  "text": {
                    "type": "mrkdwn",
                    "text": "*Repository:* ${{ github.repository }}\n*PR:* #${{ github.event.pull_request.number }} — ${{ github.event.pull_request.title }}\n*Author:* ${{ github.event.pull_request.user.login }}\n\n*Cost delta: ${{ steps.cost-gate.outputs.cost_delta }}/month* exceeds threshold of ${{ steps.cost-gate.outputs.blocked }}/month"
                  }
                },
                {
                  "type": "actions",
                  "elements": [
                    {
                      "type": "button",
                      "text": { "type": "plain_text", "text": "Review PR" },
                      "url": "${{ github.event.pull_request.html_url }}",
                      "style": "danger"
                    }
                  ]
                }
              ]
            }
        env:
          SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}

      - name: Fail if budget gate triggered
        if: steps.cost-gate.outputs.blocked == 'true'
        run: |
          echo "Budget gate triggered. Monthly cost increase of $${{ steps.cost-gate.outputs.cost_delta }} exceeds the $${{ steps.cost-gate.outputs.threshold }} threshold."
          echo "Add the 'cost-approved' label to override after budget review."
          exit 1

Step 4: Configure the Budget Threshold

Set your threshold as a GitHub Actions Variable (not a secret, it's not sensitive):

GitHub → Settings → Variables → Actions → New repository variable

| Variable | Value | |----|----| | MONTHLY_BUDGET_THRESHOLD | 50 (or whatever your team agrees on) |

The right threshold depends on your team's infrastructure maturity. A good starting point: set it to the monthly cost of one db.t3.small RDS instance (~$50). Any PR that adds more than that to your bill needs a human review.

Step 5: Add the Cost-Approved Override Label

For cases where a cost increase is deliberate and justified, scaling up for a new product launch, adding a read replica for a high-traffic feature, you need an escape valve that doesn't require changing the pipeline.

Create a GitHub label called cost-approved. Then update the Fail if budget gate triggered step:

      - name: Check for cost-approved override
        id: check-override
        uses: actions/github-script@v7
        with:
          script: |
            const labels = context.payload.pull_request.labels.map(l => l.name);
            const approved = labels.includes('cost-approved');
            core.setOutput('approved', approved);

      - name: Fail if budget gate triggered and not approved
        if: steps.cost-gate.outputs.blocked == 'true' && steps.check-override.outputs.approved != 'true'
        run: |
          echo "Budget gate triggered. Add 'cost-approved' label to override."
          exit 1

This way, an engineering lead can review a PR, agree the cost increase is justified, add the cost-approved label, and the pipeline clears, with a full audit trail in GitHub of who approved it and when.

What a PR Comment Looks Like

When a developer opens a PR that changes db.t3.micro to db.t3.small, they'll see this automatically posted to the PR:

Cloud Cost Gate: Within budget threshold

> Monthly cost delta: $14.60 (threshold: $50/month)

## Cost Breakdown

Resource                    | Monthly Cost | Change
----------------------------|-------------|--------
aws_db_instance.main        | +$14.60     | ↑ db.t3.micro → db.t3.small
aws_ecs_service.app         | $0.00       | No change
TOTAL                       | +$14.60/mo  |

Powered by Infracost + GitHub Actions

And if the change pushes past the threshold:

Cloud Cost Gate: BUDGET GATE TRIGGERED

Monthly cost delta: $187.20 (threshold: $50/month)

This PR would increase monthly cloud spend by $187.20, which exceeds 
the configured threshold of $50/month.

Please:
1. Ping @platform-team in Slack for a cost review
2. Consider whether this infrastructure change can be right-sized
3. If approved, a team lead can override by adding the cost-approved label

The engineer knows immediately what they've done, what it costs, and what to do next. That's the feedback loop that changes behaviour.

The Complete Pipeline: What You've Built Across Three Articles

Let's take a moment to look at what the full pipeline does now:

Developer pushes code / opens PR
          ↓
┌──────────────┬───────────────┬──────────────────┐
│     Lint     │  Secret Scan  │   Cost Gate       │
│  (ESLint)    │  (Gitleaks)   │  (Infracost)      │ ← parallel
└──────┬───────┴───────┬───────┴──────┬────────────┘
       │               │              │
       ▼               ▼              ▼
    Tests          SAST Scan      PR Comment +
    (Jest)         (Semgrep)      Budget Gate
       │
       ▼
     Build → Docker image → ECR
       │
       ▼
  Container Scan (Trivy)
       │
       ▼
  AI Synthesis (GPT-4o) → Slack Incident Report
       │
  Block or Pass
       │
       ▼
  Deploy Staging ← automatic if all gates pass
       │
  Manual approval
       │
       ▼
  Deploy Production

Every gate has a purpose. Every failure produces actionable output. No gate is a dead end, there's either a fix path or an override path with an audit trail.

This is what a production-grade pipeline looks like. Not the most automated thing possible. The most useful thing possible.

Key Takeaways From the Series

Part 1 taught us: Pipeline structure matters more than individual steps. Fail fast. Gate production behind human approval. Tag images with git SHAs, not latest.

Part 2 taught us: Raw scanner output is not intelligence. AI synthesis turns findings into decisions. The agent advises; the engineer decides. Separate the tools that find problems from the layer that interprets them.

Part 3 taught us: Cost is a first-class engineering concern. Embed it where decisions are made, in the PR, at the moment of change, not in a monthly finance report. Give engineers an override path, but make the default behaviour cost-aware.

The pipeline we've built together isn't just a deployment mechanism. It's a feedback system. Every push, every PR, every deployment teaches your team something about the safety and cost profile of their changes, automatically, in context, at the moment it matters.

That's the goal. That's what production-grade means.

← Back to Part 2: Adding AI-Powered Security Scanning

← Back to Part 1: Setting Up a CI/CD Pipeline From Scratch

References

[1] Flexera. 2026 State of the Cloud Report. April 2026. https://info.flexera.com/CM-REPORT-State-of-the-Cloud

[2] Infracost. Infracost Documentation — GitHub Actions Integration. https://www.infracost.io/docs/integrations/github_actions/

[3] Infracost. Infracost Cloud Pricing API. https://www.infracost.io/docs/supported_resources/aws/

[4] HashiCorp. Terraform AWS Provider Documentation. https://registry.terraform.io/providers/hashicorp/aws/latest/docs

[5] GitHub. GitHub Actions — Variables and Secrets. https://docs.github.com/en/actions/security-guides/using-secrets-in-github-actions

[6] DORA. Accelerate State of DevOps Report 2024. https://dora.dev/research/2024/dora-report/

\


This content originally appeared on HackerNoon and was authored by emmanuela Opurum


Print Share Comment Cite Upload Translate Updates
APA

emmanuela Opurum | Sciencx (2026-05-12T13:00:34+00:00) Building a Production-Grade CI/CD Pipeline — Part 3: Adding Cloud Cost Optimization. Retrieved from https://www.scien.cx/2026/05/12/building-a-production-grade-ci-cd-pipeline-part-3-adding-cloud-cost-optimization/

MLA
" » Building a Production-Grade CI/CD Pipeline — Part 3: Adding Cloud Cost Optimization." emmanuela Opurum | Sciencx - Tuesday May 12, 2026, https://www.scien.cx/2026/05/12/building-a-production-grade-ci-cd-pipeline-part-3-adding-cloud-cost-optimization/
HARVARD
emmanuela Opurum | Sciencx Tuesday May 12, 2026 » Building a Production-Grade CI/CD Pipeline — Part 3: Adding Cloud Cost Optimization., viewed ,<https://www.scien.cx/2026/05/12/building-a-production-grade-ci-cd-pipeline-part-3-adding-cloud-cost-optimization/>
VANCOUVER
emmanuela Opurum | Sciencx - » Building a Production-Grade CI/CD Pipeline — Part 3: Adding Cloud Cost Optimization. [Internet]. [Accessed ]. Available from: https://www.scien.cx/2026/05/12/building-a-production-grade-ci-cd-pipeline-part-3-adding-cloud-cost-optimization/
CHICAGO
" » Building a Production-Grade CI/CD Pipeline — Part 3: Adding Cloud Cost Optimization." emmanuela Opurum | Sciencx - Accessed . https://www.scien.cx/2026/05/12/building-a-production-grade-ci-cd-pipeline-part-3-adding-cloud-cost-optimization/
IEEE
" » Building a Production-Grade CI/CD Pipeline — Part 3: Adding Cloud Cost Optimization." emmanuela Opurum | Sciencx [Online]. Available: https://www.scien.cx/2026/05/12/building-a-production-grade-ci-cd-pipeline-part-3-adding-cloud-cost-optimization/. [Accessed: ]
rf:citation
» Building a Production-Grade CI/CD Pipeline — Part 3: Adding Cloud Cost Optimization | emmanuela Opurum | Sciencx | https://www.scien.cx/2026/05/12/building-a-production-grade-ci-cd-pipeline-part-3-adding-cloud-cost-optimization/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.