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.
Target State
-
First
terraform applyalready 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)
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