Back to Blog

Cost Optimization Patterns in Terraform: Dev vs Prod SKU Strategies

January 3, 2026
Stefan Mentović
terraformazurecost-optimizationinfrastructuredevops

Cut Azure infrastructure costs by 40%+ using environment-based SKU selection, shared resources, and smart tier strategies in Terraform.

Your development environment costs as much as production. Sound familiar?

Many teams waste thousands of dollars running oversized resources in dev environments that barely see traffic. A production-grade PostgreSQL instance running 24/7 for a single developer. A premium Redis cache for testing. Multiple AI service accounts across environments when one would do.

The solution isn't cutting corners on infrastructure quality. It's using Terraform to implement intelligent resource sizing strategies that maintain performance where it matters while dramatically reducing costs where it doesn't.

After implementing these patterns across multiple Azure projects, we've seen consistent 40-50% cost reductions in development environments without sacrificing developer experience. Let's explore how.

#The Real Cost of Environment Parity

The traditional DevOps wisdom of "prod-dev parity" has merit for deployment pipelines and application code. But infrastructure resources? That's where the advice breaks down.

Consider a typical Azure setup:

  • PostgreSQL Flexible Server (GP_Standard_D2s_v3): ~$100/month
  • Azure Cache for Redis (Standard C1): ~$70/month
  • Container Apps Environment: ~$40/month
  • AI Services account: ~$50/month

Multiply by three environments (dev, staging, prod), and you're at ~$780/month before you've deployed a single line of code.

The insight: Development traffic patterns are nothing like production. Dev environments sit idle 60-80% of the time. They don't need premium SKUs, high-availability configurations, or production-scale capacity.

#Strategy 1: Environment-Based SKU Selection

The cornerstone of cost optimization is matching resource tiers to actual usage patterns. Terraform locals make this pattern elegant:

locals {
  # PostgreSQL SKU configuration:
  # Prod: GP_Standard_D2s_v3 (General Purpose, 2 vCores, 8GB RAM) - ~$100/mo
  # Dev:  B_Standard_B2s (Burstable, 2 vCores, 4GB RAM) - ~$60/mo
  # Savings: ~$40/month while maintaining 2 vCore performance
  postgres_sku = var.env == "prod" ? "GP_Standard_D2s_v3" : "B_Standard_B2s"
}

resource "azurerm_postgresql_flexible_server" "database" {
  name                = "${var.prefix}-${var.env}-postgres"
  resource_group_name = azurerm_resource_group.main.name
  location            = var.location

  # Dynamic SKU based on environment
  sku_name = local.postgres_sku

  # Same configuration otherwise
  storage_mb = var.env == "prod" ? 32768 : 16384

  tags = {
    Environment = var.env
  }
}

#Why This Works

Burstable (B-series) for Dev:

  • Accumulates CPU credits during idle time
  • Bursts to full performance when needed
  • Perfect for intermittent dev workloads
  • ~60% cost reduction vs General Purpose

General Purpose (GP-series) for Prod:

  • Consistent baseline performance
  • Higher memory for production workloads
  • Better suited for 24/7 traffic

The key insight: Development databases spend most of their time idle. B-series instances accumulate credits during quiet periods and burst to full speed when developers actually run queries. You get the performance when you need it at a fraction of the cost.

#Strategy 2: Shared Resource Architecture

Not every resource needs to be duplicated across environments. Some services are perfect candidates for cross-environment sharing, especially when traffic is minimal.

#Case Study: Bing Grounding API

Consider Bing API integration. You're making maybe 100 calls/day in dev, 200 in staging, and 10,000 in production. Why pay for three separate Bing accounts?

# Bing resource group - created once by dev, shared with prod
resource "azurerm_resource_group" "bing_grounding" {
  count    = var.env == "dev" ? 1 : 0
  provider = azurerm.bing
  name     = "${var.prefix}-shared-bing-rg"
  location = "westeurope"
}

# Prod imports existing resource
data "azurerm_resource_group" "bing_grounding_existing" {
  count    = var.env == "prod" ? 1 : 0
  provider = azurerm.bing
  name     = "${var.prefix}-shared-bing-rg"
}

# Local to access resource regardless of environment
locals {
  bing_rg_id = one(azurerm_resource_group.bing_grounding[*].id) != null ?
    one(azurerm_resource_group.bing_grounding[*].id) :
    one(data.azurerm_resource_group.bing_grounding_existing[*].id)
}

# Bing Grounding resource - created once, shared by all
resource "azapi_resource" "bing_grounding" {
  count     = var.env == "dev" ? 1 : 0
  type      = "Microsoft.Bing/accounts@2020-06-10"
  name      = "${var.prefix}-shared-bing-grounding"
  parent_id = local.bing_rg_id
  location  = "global"

  body = {
    kind = "Bing.Grounding"
    sku = {
      name = "G1"
    }
  }

  tags = {
    purpose = "shared-grounding"
    note    = "Shared between dev and prod for cost optimization"
  }
}

#The Pattern

  • Dev environment creates the shared resource
  • Other environments import via data sources
  • Both reference the same local variable
  • Tag clearly to prevent accidental deletion

Savings: ~$50-100/month depending on the service.

When to share:

  • Low-volume APIs (Bing, weather services, geolocation)
  • External service integrations with generous quotas
  • Monitoring and logging services (with proper filtering)

When NOT to share:

  • Databases (isolation is critical)
  • Authentication services (security boundary)
  • High-traffic APIs (quota contention)

#Strategy 3: Redis Tier Selection for Use Case

Azure Cache for Redis offers multiple tiers, but most teams default to Standard or Premium without considering their actual requirements.

locals {
  # Redis configuration - Basic tier for job queues
  # No data persistence needed (jobs can be re-queued from database)
  redis_config = {
    sku_name            = "Basic"
    family              = "C"
    capacity            = 0 # C0 = 250MB (~$16/month)
    minimum_tls_version = "1.2"
    enable_aof_backup   = false # No persistence on Basic tier
  }
}

resource "azurerm_redis_cache" "queue" {
  name                = "${var.prefix}-${var.env}-redis"
  location            = azurerm_resource_group.main.location
  resource_group_name = azurerm_resource_group.main.name

  sku_name = local.redis_config.sku_name
  family   = local.redis_config.family
  capacity = local.redis_config.capacity

  redis_configuration {
    # Critical for job queues - prevent data loss by returning errors
    # instead of evicting keys when memory is full
    maxmemory_policy = "noeviction"
  }

  tags = {
    UseCase = "job-queue"
  }
}

#Understanding Redis Tiers

Basic C0 (~$16/month):

  • Perfect for: Job queues, rate limiting, simple caching
  • Limitations: No replication, no persistence, 250MB
  • Reality check: Most job queues use less than 100MB

Standard C1 (~$70/month):

  • Perfect for: Session storage, application caching with HA
  • Benefits: Replication for availability, persistence options
  • When needed: Production data that can't be regenerated

Premium (~$300+/month):

  • Perfect for: Critical systems requiring clustering and geo-replication
  • Benefits: Multi-region, VNet injection, larger sizes
  • When needed: High-throughput production systems

#The Key Question: Can You Regenerate the Data?

For job queues (BullMQ, Celery, etc.), the answer is yes. Jobs are already persisted in your database. Redis is just the work distribution mechanism. If Redis restarts, jobs get re-queued from the source.

This means:

  • No need for AOF persistence
  • No need for replication
  • Basic tier is perfect

Savings: ~$54/month per environment (Basic vs Standard C1)

#Strategy 4: Container Apps vs AKS Cost Analysis

Kubernetes (AKS) has become the default for container orchestration, but Azure Container Apps offers significant cost advantages for many workloads.

#The Cost Comparison

AKS (Kubernetes):

  • Cluster management: Free
  • Nodes: 3x Standard_D2s_v3 (~$70/month each) = ~$210/month
  • Load balancer: ~$20/month
  • Public IP: ~$3/month
  • Total: ~$233/month (before any workloads)

Container Apps:

  • No idle cost in Consumption mode
  • Pay only for CPU/memory usage: ~$30-60/month typical dev workload
  • Managed ingress, scaling, certificates included
  • Total: ~$30-60/month
# Container Apps Environment - Consumption plan
resource "azurerm_container_app_environment" "main" {
  name                       = "${var.prefix}-container-environment"
  location                   = azurerm_resource_group.main.location
  resource_group_name        = azurerm_resource_group.main.name
  log_analytics_workspace_id = azurerm_log_analytics_workspace.main.id

  # VNet integration - environment-specific
  # Dev: Disabled (public endpoints for simplicity)
  # Prod: Enabled (private endpoints for security)
  infrastructure_subnet_id = var.env == "prod" ? azurerm_subnet.container_apps.id : null
}

resource "azurerm_container_app" "api" {
  name                         = "${var.prefix}-${var.env}-api"
  container_app_environment_id = azurerm_container_app_environment.main.id
  resource_group_name          = azurerm_resource_group.main.name
  revision_mode                = "Multiple"

  template {
    container {
      name   = "api"
      image  = "${azurerm_container_registry.main.login_server}/api:latest"
      cpu    = 0.25
      memory = "0.5Gi"

      env {
        name  = "DATABASE_URL"
        secret_name = "database-url"
      }
    }

    min_replicas = 0  # Scale to zero in dev
    max_replicas = var.env == "prod" ? 10 : 2
  }

  ingress {
    external_enabled = true
    target_port      = 3000

    traffic_weight {
      latest_revision = true
      percentage      = 100
    }
  }
}

#When to Choose Container Apps

Perfect for:

  • API backends without complex service mesh needs
  • Web applications with variable traffic
  • Microservices that can scale independently
  • Development environments (scale to zero)

Stick with AKS when:

  • You need advanced K8s features (StatefulSets, DaemonSets)
  • You're already running K8s elsewhere (skill reuse)
  • You need multi-cloud portability
  • You require custom CNI or networking

Dev environment savings: ~$170-200/month per environment

#Strategy 5: PostgreSQL Flexible Server Optimization

PostgreSQL Flexible Server offers better price/performance than older Single Server, but tier selection matters enormously.

locals {
  # PostgreSQL configuration with intelligent sizing
  postgres_config = {
    prod = {
      sku_name    = "GP_Standard_D2s_v3"  # General Purpose, 2 vCore
      storage_mb  = 32768                  # 32 GB
      backup_days = 35                     # Max retention
      geo_backup  = true                   # Geo-redundant
    }
    dev = {
      sku_name    = "B_Standard_B2s"      # Burstable, 2 vCore
      storage_mb  = 16384                  # 16 GB (plenty for dev)
      backup_days = 7                      # Minimum retention
      geo_backup  = false                  # Local backup only
    }
  }
}

resource "azurerm_postgresql_flexible_server" "main" {
  name                   = "${var.prefix}-${var.env}-postgres"
  resource_group_name    = azurerm_resource_group.main.name
  location               = var.location
  version                = "14"

  # Use environment-specific configuration
  sku_name               = local.postgres_config[var.env].sku_name
  storage_mb             = local.postgres_config[var.env].storage_mb

  backup_retention_days  = local.postgres_config[var.env].backup_days
  geo_redundant_backup_enabled = local.postgres_config[var.env].geo_backup

  # Authentication
  administrator_login    = "dbadmin"
  administrator_password = random_password.postgres.result

  # High availability - prod only
  dynamic "high_availability" {
    for_each = var.env == "prod" ? [1] : []
    content {
      mode = "ZoneRedundant"
    }
  }
}

#Cost Breakdown

Development (B_Standard_B2s):

  • Base cost: ~$60/month
  • Storage (16GB): ~$2/month
  • Backups (7 days): ~$1/month
  • Total: ~$63/month

Production (GP_Standard_D2s_v3 with HA):

  • Base cost: ~$100/month
  • Storage (32GB): ~$4/month
  • Backups (35 days, geo-redundant): ~$8/month
  • High Availability: ~$100/month (doubles base cost)
  • Total: ~$212/month

Key optimizations:

  • Burstable tier for dev - matches usage pattern
  • Minimal backup retention in dev - 7 days vs 35
  • No geo-redundancy in dev - local backups sufficient
  • No HA in dev - acceptable downtime for development

Savings: ~$149/month per dev environment

#Strategy 6: Conditional Resource Creation

Some resources simply aren't needed in all environments. Use Terraform's count and for_each to create resources conditionally.

# Private endpoints - prod only
resource "azurerm_private_endpoint" "redis" {
  count = var.env == "prod" ? 1 : 0

  name                = "${var.prefix}-redis-private-endpoint"
  location            = azurerm_resource_group.main.location
  resource_group_name = azurerm_resource_group.main.name
  subnet_id           = azurerm_subnet.redis.id

  private_service_connection {
    name                           = "${var.prefix}-redis-private-connection"
    private_connection_resource_id = azurerm_redis_cache.main.id
    subresource_names              = ["redisCache"]
    is_manual_connection           = false
  }

  private_dns_zone_group {
    name                 = "redis-dns-zone-group"
    private_dns_zone_ids = [azurerm_private_dns_zone.redis[0].id]
  }
}

# Private DNS zones - prod only
resource "azurerm_private_dns_zone" "redis" {
  count = var.env == "prod" ? 1 : 0

  name                = "privatelink.redis.cache.windows.net"
  resource_group_name = azurerm_resource_group.main.name
}

# VNet integration - prod only
resource "azurerm_container_app_environment" "main" {
  name                = "${var.prefix}-container-environment"
  location            = azurerm_resource_group.main.location
  resource_group_name = azurerm_resource_group.main.name

  # Dev: Public endpoints (simpler, cheaper)
  # Prod: VNet integration (secure, private endpoints)
  infrastructure_subnet_id = var.env == "prod" ? azurerm_subnet.container_apps.id : null
}

# Init containers - dev only
resource "azurerm_container_app" "web" {
  # ... main configuration ...

  template {
    # Database seeding - dev only
    dynamic "init_container" {
      for_each = var.env == "dev" ? [1] : []
      content {
        name   = "seed"
        image  = "${azurerm_container_registry.main.login_server}/seed:latest"
        cpu    = "0.25"
        memory = "0.5Gi"

        command = ["npm", "run", "db:seed"]

        env {
          name        = "DATABASE_URL"
          secret_name = "database-url"
        }
      }
    }

    # Main container
    container {
      # ... configuration ...
    }
  }
}

#Resources to Skip in Dev

Private Endpoints (~$10-15 each/month):

  • Dev can use public endpoints with IP restrictions
  • Saves network complexity and cost

Storage Accounts for Backups:

  • Basic Redis tier doesn't support persistence
  • Don't create unused storage accounts

Multiple Regions:

  • Dev runs in single region
  • Prod might need geo-replication

Load Balancers:

  • Container Apps includes ingress
  • AKS dev clusters can use NodePort

Data Seeding:

  • Use init containers in dev only
  • Production data comes from migrations

Savings: ~$30-50/month per environment

#Strategy 7: Real Cost Savings Examples

Let's calculate actual savings from implementing these patterns across a typical Azure application.

#Before Optimization: Everything Production-Grade

Development Environment:

  • PostgreSQL GP_Standard_D2s_v3: ~$100/month
  • Redis Standard C1: ~$70/month
  • AKS (3 nodes): ~$210/month
  • Bing API account: ~$50/month
  • Private endpoints (3): ~$30/month
  • Storage accounts: ~$15/month
  • Total: ~$475/month

Production Environment:

  • PostgreSQL GP_Standard_D2s_v3 + HA: ~$212/month
  • Redis Standard C1: ~$70/month
  • AKS (3 nodes): ~$210/month
  • Bing API account: ~$50/month
  • Private endpoints (3): ~$30/month
  • Storage accounts: ~$15/month
  • Total: ~$587/month

Combined: ~$1,062/month

#After Optimization: Right-Sized Infrastructure

Development Environment:

  • PostgreSQL B_Standard_B2s: ~$63/month
  • Redis Basic C0: ~$16/month
  • Container Apps: ~$50/month
  • Bing API (shared): $0/month
  • Private endpoints: $0/month
  • Storage accounts: $0/month
  • Total: ~$129/month

Production Environment:

  • PostgreSQL GP_Standard_D2s_v3 + HA: ~$212/month
  • Redis Basic C0: ~$16/month (queue use case)
  • Container Apps: ~$120/month
  • Bing API (shared): ~$50/month
  • Private endpoints (2): ~$20/month
  • Total: ~$418/month

Combined: ~$547/month

#The Numbers

  • Total savings: ~$515/month (~$6,180/year)
  • Dev environment reduction: 73% (~$346/month saved)
  • Prod environment optimization: 29% (~$169/month saved)
  • ROI: Terraform refactoring pays for itself in week one

#Implementation Guidelines

#Start with Variables

Create an environment-agnostic configuration:

variable "env" {
  description = "Environment (dev, staging, prod)"
  type        = string
  validation {
    condition     = contains(["dev", "staging", "prod"], var.env)
    error_message = "Environment must be dev, staging, or prod."
  }
}

variable "prefix" {
  description = "Resource prefix for naming"
  type        = string
}

variable "location" {
  description = "Azure region"
  type        = string
  default     = "westeurope"
}

#Use Locals for Configuration Maps

locals {
  # Centralized configuration by environment
  env_config = {
    dev = {
      postgres_sku     = "B_Standard_B2s"
      postgres_storage = 16384
      redis_sku        = "Basic"
      redis_capacity   = 0
      container_cpu    = 0.25
      container_memory = "0.5Gi"
      enable_ha        = false
      enable_vnet      = false
      min_replicas     = 0
      max_replicas     = 2
    }
    prod = {
      postgres_sku     = "GP_Standard_D2s_v3"
      postgres_storage = 32768
      redis_sku        = "Basic"
      redis_capacity   = 0
      container_cpu    = 0.5
      container_memory = "1Gi"
      enable_ha        = true
      enable_vnet      = true
      min_replicas     = 2
      max_replicas     = 10
    }
  }

  # Easy access to current environment config
  config = local.env_config[var.env]
}

# Use in resources
resource "azurerm_postgresql_flexible_server" "main" {
  sku_name   = local.config.postgres_sku
  storage_mb = local.config.postgres_storage
  # ... rest of configuration
}

#Tag Everything

locals {
  common_tags = {
    Environment = var.env
    ManagedBy   = "terraform"
    Project     = var.prefix
    CostCenter  = var.env == "prod" ? "production" : "development"
  }
}

# Apply to all resources
resource "azurerm_resource_group" "main" {
  tags = local.common_tags
}

#Document Your Decisions

locals {
  # PostgreSQL SKU configuration:
  # Prod: GP_Standard_D2s_v3 (General Purpose, 2 vCores, 8GB RAM) - ~$100/mo
  # Dev:  B_Standard_B2s (Burstable, 2 vCores, 4GB RAM) - ~$60/mo
  # Savings: ~$40/month while maintaining 2 vCore performance
  postgres_sku = var.env == "prod" ? "GP_Standard_D2s_v3" : "B_Standard_B2s"
}

Comments explain the "why" behind cost decisions. Future you will appreciate it.

#Test in Dev First

# Plan changes in dev
terraform plan -var-file="environments/dev.tfvars"

# Apply to dev
terraform apply -var-file="environments/dev.tfvars"

# Monitor costs for a week
az consumption usage list --start-date 2026-01-03 --end-date 2026-01-10

# If successful, promote to prod
terraform plan -var-file="environments/prod.tfvars"
terraform apply -var-file="environments/prod.tfvars"

#Monitor Cost Impact

Use Azure Cost Management to track savings:

# Create cost analysis view
az costmanagement query \
  --type Actual \
  --dataset-grouping name="ResourceGroup" type="Dimension" \
  --timeframe MonthToDate

Tag resources consistently to enable cost tracking by environment, team, or project.

#Key Takeaways

Cost optimization isn't about cutting corners. It's about matching resources to actual usage patterns:

  • Environment-based SKU selection reduces database costs by 40-60% in dev without impacting performance
  • Shared resources for low-volume APIs eliminate duplicate subscription costs
  • Redis tier selection based on use case (job queues don't need persistence) cuts caching costs by 75%
  • Container Apps vs AKS for simple workloads saves ~$200+/month per environment
  • PostgreSQL Burstable tier perfect for intermittent dev workloads, accumulates credits during idle time
  • Conditional resources via count/for_each prevents creating unnecessary infrastructure
  • Real savings of 40-70% in development, 20-30% in production are achievable

The patterns in this guide have been tested in production across multiple Azure projects. They maintain infrastructure quality where it matters while dramatically reducing wasted spend.

Ready to optimize your infrastructure costs? Start with the Azure Pricing Calculator to model your current and optimized architecture. Then explore the Terraform Azure Provider documentation for implementation details.

Your cloud bill will thank you.

Enjoyed this article? Stay updated: