WAF++ WAF++
Back to WAF++ Homepage

Best Practice: Greenfield FinOps by Design

Context

Greenfield projects have a unique advantage: there is no cost debt. Every decision can be made from the start with full cost awareness. This advantage is frequently squandered because FinOps is treated as a "phase 2" topic – after launch, when the cost structures are already locked in.

FinOps by design means: cost controls are part of the first commit, not the second retrospective.

All WAF-COST controls – with focus on preventive rather than corrective measures.

Target State

  • First terraform apply already includes: mandatory tag module, budget resource, lifecycle policies

  • ADR template with cost impact section is available from the start and is used

  • FinOps review cycle starts 30 days after launch (not "at some point")

  • No resource goes live without full tagging compliance

Platform Template: Everything from Day 0

Repository Base Structure

new-service/
├── docs/
│   ├── adr/
│   │   └── ADR-TEMPLATE.md          # With cost impact section
│   ├── cost-debt-register.yml        # Initialized, empty
│   └── retention-strategy.yml        # Tier strategy documented
├── infrastructure/
│   ├── modules/
│   │   ├── mandatory-tags/           # Required: cost-center, owner, etc.
│   │   └── budget-alert/             # Required: budget + alert
│   ├── environments/
│   │   ├── production/
│   │   │   ├── main.tf
│   │   │   ├── budget.tf             # Production budget
│   │   │   └── variables.tf
│   │   └── staging/
│   │       └── ...
│   └── shared/
│       └── lifecycle-defaults.tf     # Default lifecycle for storage/logs
├── .github/
│   └── workflows/
│       ├── cost-compliance.yml       # CI gate: tagging, lifecycle, budget
│       └── monthly-finops-report.yml
└── tagging-taxonomy.yml

Mandatory Tags Module (complete)

# modules/mandatory-tags/main.tf

variable "cost_center" {
  type = string
  validation {
    condition     = can(regex("^[a-z][a-z0-9-]+$", var.cost_center))
    error_message = "cost-center must be lowercase kebab-case."
  }
}

variable "owner" {
  type = string
  validation {
    condition     = can(regex("^[a-z][a-z0-9-]+$", var.owner))
    error_message = "owner must be lowercase kebab-case (team name, not person)."
  }
}

variable "environment" {
  type = string
  validation {
    condition     = contains(["production", "staging", "development", "testing"], var.environment)
    error_message = "environment must be: production, staging, development, or testing."
  }
}

variable "workload" {
  type        = string
  description = "Service or workload name."
}

variable "additional_tags" {
  type    = map(string)
  default = {}
}

locals {
  base_tags = {
    cost-center          = var.cost_center
    owner                = var.owner
    environment          = var.environment
    workload             = var.workload
    managed-by           = "terraform"
    wafpp-cost-compliant = "true"
  }
}

output "tags" {
  value = merge(local.base_tags, var.additional_tags)
}

Budget Module as Mandatory Component

# modules/budget-alert/main.tf

variable "budget_name" {
  type = string
}

variable "monthly_limit" {
  type        = number
  description = "Monthly budget limit in USD."
}

variable "workload" {
  type = string
}

variable "alert_emails" {
  type        = list(string)
  description = "Email addresses for budget alerts."
}

resource "aws_budgets_budget" "workload_budget" {
  name         = "${var.budget_name}-monthly"
  budget_type  = "COST"
  limit_amount = tostring(var.monthly_limit)
  limit_unit   = "USD"
  time_unit    = "MONTHLY"

  cost_filter {
    name   = "TagKeyValue"
    values = ["workload$${var.workload}"]
  }

  notification {
    comparison_operator        = "GREATER_THAN"
    threshold                  = 80
    threshold_type             = "PERCENTAGE"
    notification_type          = "ACTUAL"
    subscriber_email_addresses = var.alert_emails
  }

  notification {
    comparison_operator        = "GREATER_THAN"
    threshold                  = 100
    threshold_type             = "PERCENTAGE"
    notification_type          = "ACTUAL"
    subscriber_email_addresses = var.alert_emails
  }

  notification {
    comparison_operator        = "GREATER_THAN"
    threshold                  = 110
    threshold_type             = "PERCENTAGE"
    notification_type          = "FORECASTED"
    subscriber_email_addresses = var.alert_emails
  }
}

Lifecycle Defaults as Standard

# shared/lifecycle-defaults.tf – Always for all environments

# CloudWatch Log Groups MUST have retention
# Passed as a variable; default varies by environment

variable "log_retention_operational" {
  type        = number
  default     = 30
  description = "Retention in days for operational logs (Hot-Tier)."
  validation {
    condition     = var.log_retention_operational > 0
    error_message = "Log retention must be > 0 days. 0 means infinite (not allowed)."
  }
}

variable "log_retention_audit" {
  type        = number
  default     = 365
  description = "Retention in days for audit logs (Cold-Tier)."
}

# S3 lifecycle policy module (mandatory for all buckets)
module "s3_lifecycle" {
  source  = "../../modules/s3-lifecycle"
  bucket  = aws_s3_bucket.main.id

  transition_to_ia_days       = 30
  transition_to_glacier_days  = 90
  expiration_days             = 365  # Adjust per data class
  delete_old_versions_after   = 30
}

Complete Production Environment Example

# environments/production/main.tf

module "tags" {
  source = "../../modules/mandatory-tags"

  cost_center = "fintech-platform"
  owner       = "payments-team"
  environment = "production"
  workload    = "payment-service"
}

module "budget" {
  source = "../../modules/budget-alert"

  budget_name   = "payment-service-prod"
  monthly_limit = 5000  # USD/month
  workload      = "payment-service"
  alert_emails  = ["payments-team@company.com", "finops@company.com"]
}

resource "aws_instance" "app" {
  ami           = data.aws_ami.ubuntu.id
  instance_type = "t3.medium"

  tags = merge(module.tags.tags, {
    rightsizing-reviewed = "2025-03-01"
    capacity-commitment  = "on-demand"
  })
}

resource "aws_cloudwatch_log_group" "app" {
  name              = "/app/production/payment-service"
  retention_in_days = 30  # Hot tier: 30 days
  kms_key_id        = aws_kms_key.logging.arn
  tags              = module.tags.tags
}

resource "aws_s3_bucket" "data" {
  bucket = "acme-payment-data-prod"
  tags   = module.tags.tags
}

resource "aws_s3_bucket_lifecycle_configuration" "data" {
  bucket = aws_s3_bucket.data.id

  rule {
    id     = "default-tiering"
    status = "Enabled"

    transition {
      days          = 30
      storage_class = "STANDARD_IA"
    }

    transition {
      days          = 90
      storage_class = "GLACIER_IR"
    }

    noncurrent_version_expiration {
      noncurrent_days = 30
    }
  }
}

Greenfield Checklist: Pre-Launch Gate

Before a service goes to production, this checklist must be fulfilled:

Tagging & Budget (WAF-COST-010, WAF-COST-020)

  • Mandatory tag module used in all resources

  • Tagging compliance: 100% of all resources

  • Budget resource defined as IaC

  • 80% and 100% alerts configured

  • Alert recipients: team channel + FinOps

Lifecycle & Retention (WAF-COST-040, WAF-COST-070)

  • All S3 buckets have a lifecycle configuration

  • All CloudWatch Log Groups have retention_in_days != 0

  • Retention strategy document present

  • Log tier documented (Hot/Warm/Cold/Archive)

Architecture & Cost Debt (WAF-COST-050, WAF-COST-100)

  • Initial ADR for infrastructure decisions present

  • ADR contains cost impact assessment

  • Cost debt register initialized (even if empty)

  • FinOps review date entered 30 days after first deploy

FinOps Processes (WAF-COST-060)

  • FinOps review cycle entered in team calendar (first review after 30 days)

  • Rightsizing review planned after 90 days

Metrics (Greenfield-Specific)

  • Time to compliance: days from first deploy to 100% cost compliance (target: 0 – from day 0)

  • Cost growth rate months 1–6: % deviation from initial TCO estimate (target: < ±20%)

  • First rightsizing action: no later than 90 days after launch

  • First FinOps review: no later than 30 days after launch