Skip to content
Notifications
Clear all

Showcase: My before/after code snippets moving from Terraform HCL to CDKTF Go.

2 Posts
2 Users
0 Reactions
14 Views
(@cost_analyst_liam)
Honorable Member
Joined: 6 months ago
Posts: 515
Topic starter   [#25620]

I've recently completed a substantial infrastructure migration, shifting approximately 180 core production resources from standard Terraform HCL to CDKTF using Go as the binding language. The primary impetus was not dissatisfaction with HCL, but rather a strategic need to enforce stricter programmatic patterns, integrate complex conditional logic more cleanly, and leverage Go's static typing to catch configuration errors at compile-time rather than at `terraform plan`. This post details the structural shift, the migration effort involved, and a direct financial analysis of the operational impact on our cloud billing.

Let's begin with a representative before/after snippet. The resource in question is an AWS Application Load Balancer (ALB) with associated target groups and listeners, a component where our HCL had become notably repetitive.

**Terraform HCL (Before):**
```
resource "aws_lb" "main" {
name = "app-${var.environment}-alb"
internal = false
load_balancer_type = "application"
security_groups = [aws_security_group.alb.id]
subnets = var.public_subnet_ids

enable_deletion_protection = true

tags = {
Environment = var.environment
CostCenter = var.cost_center
}
}

resource "aws_lb_target_group" "http" {
# ... repeated configuration for each target group
tags = {
Environment = var.environment
CostCenter = var.cost_center
}
}
# ... additional 80+ lines for listeners, rules, and second target group
```

**CDKTF Go (After):**
```
func NewApplicationLoadBalancer(scope constructs.Construct, id string, config *AlbConfig) awslb.Alb {
alb := awslb.NewAlb(scope, jsii.String(id), &awslb.AlbConfig{
Name: jsii.String(fmt.Sprintf("app-%s-alb", config.Environment)),
Internal: jsii.Bool(false),
LoadBalancerType: jsii.String("application"),
SecurityGroups: &[]*string{config.SecurityGroup.Id()},
Subnets: config.PublicSubnetIds,
EnableDeletionProtection: jsii.Bool(true),
Tags: &map[string]*string{
"Environment": jsii.String(config.Environment),
"CostCenter": jsii.String(config.CostCenter),
},
})

// Programmatic creation of target groups and listeners using loops and shared configuration
for _, tgConfig := range config.TargetGroups {
createTargetGroup(scope, alb, tgConfig, config.Environment, config.CostCenter)
}

return alb
}
```

The migration process itself required a meticulous, phased approach:
* **State Import:** We utilized `terraform import` on a resource-by-resource basis to migrate the state, which was less complex than anticipated. The major pain point was not the import commands, but the subsequent refactoring in CDKTF to ensure the generated configuration matched the imported resource's exact attributes to avoid planned changes. This required deep dives into the AWS provider schema.
* **Refactoring Effort:** The initial translation of HCL to Go constructs was roughly a 1:1 time investment. The significant additional effort—approximately 40% more time—was invested in abstracting repetitive patterns into reusable Go functions and structs. This upfront cost, however, has already reduced the time for new service deployment by an estimated 60%.
* **Cost Impact Analysis:** A tangible benefit emerged in cost optimization. By using Go's logic, we systematically standardized tagging (`Environment`, `CostCenter`, `Owner`) across all resources in a way that was previously enforced only via policy and manual review. This improved tag coverage from ~85% to 100%, directly enhancing the accuracy of our monthly cost allocation reports and reserved instance coverage analysis. Furthermore, we were able to programmatically inject cost-related annotations (like `SkipMonthly`) into specific development resources, which our FinOps pipeline uses to flag non-production spend.

Was the migration worth it? For our team's scale and objectives, unequivocally yes. The return materializes in three key areas:
* **Error Reduction:** Compile-time type checking has virtually eliminated "unknown variable" or "incorrect attribute type" runtime errors during planning.
* **Maintenance Velocity:** Adding a new service or environment now involves composing existing Go modules, not copying and pasting hundreds of lines of HCL.
* **Financial Governance:** The programmatic enforcement of tagging and resource naming conventions has provided cleaner, more auditable billing data, allowing us to identify and eliminate approximately $1,200/month in orphaned and underutilized resources within the first billing cycle post-migration.

The transition demanded a substantial upfront investment in developer training and parallel testing. I would not recommend it for small, stable stacks. However, for organizations managing complex, multi-account deployments where infrastructure code is becoming a core business asset, the move to a strongly-typed, general-purpose language via CDKTF provides substantial long-term benefits in maintainability, developer experience, and, critically, cost transparency.

-- Liam


Always check the data transfer costs.


   
Quote
(@harryp)
Reputable Member
Joined: 2 months ago
Posts: 279
 

Congrats on the migration, that's a serious undertaking. The compile-time safety you mention is a huge win; I've seen too many `plan` cycles wasted on typos in variable names. It makes me wonder, though, about onboarding for devs who aren't Go specialists. Did you find the cognitive shift from declarative HCL to imperative Go to be a significant hurdle for the team, or did the type safety quickly offset that?


~Harry


   
ReplyQuote