Back to Journal
DevOps

Complete Guide to Infrastructure as Code with Go

A comprehensive guide to implementing Infrastructure as Code using Go, covering architecture, code examples, and production-ready patterns.

Muneer Puthiya Purayil 17 min read

Go's strong typing, fast compilation, and excellent concurrency model make it a compelling choice for Infrastructure as Code through Pulumi's Go SDK and custom infrastructure tooling. This guide covers implementing IaC with Go using Pulumi for cloud resource management, custom providers, and testing patterns.

Pulumi with Go

Pulumi's Go SDK provides type-safe infrastructure definitions with the full power of Go's standard library:

go
1package main
2 
3import (
4 "github.com/pulumi/pulumi-aws/sdk/v6/go/aws/ec2"
5 "github.com/pulumi/pulumi-aws/sdk/v6/go/aws/ecs"
6 "github.com/pulumi/pulumi-aws/sdk/v6/go/aws/rds"
7 "github.com/pulumi/pulumi/sdk/v3/go/pulumi"
8 "github.com/pulumi/pulumi/sdk/v3/go/pulumi/config"
9)
10 
11func main() {
12 pulumi.Run(func(ctx *pulumi.Context) error {
13 cfg := config.New(ctx, "")
14 env := cfg.Require("environment")
15
16 // VPC
17 vpc, err := ec2.NewVpc(ctx, "main", &ec2.VpcArgs{
18 CidrBlock: pulumi.String("10.0.0.0/16"),
19 EnableDnsHostnames: pulumi.Bool(true),
20 EnableDnsSupport: pulumi.Bool(true),
21 Tags: pulumi.StringMap{
22 "Name": pulumi.Sprintf("%s-vpc", env),
23 "Environment": pulumi.String(env),
24 },
25 })
26 if err != nil {
27 return err
28 }
29 
30 // Private subnets across AZs
31 azs := []string{"us-east-1a", "us-east-1b", "us-east-1c"}
32 var privateSubnetIDs pulumi.StringArray
33
34 for i, az := range azs {
35 subnet, err := ec2.NewSubnet(ctx, fmt.Sprintf("private-%d", i), &ec2.SubnetArgs{
36 VpcId: vpc.ID(),
37 CidrBlock: pulumi.Sprintf("10.0.%d.0/24", i+1),
38 AvailabilityZone: pulumi.String(az),
39 Tags: pulumi.StringMap{
40 "Name": pulumi.Sprintf("%s-private-%s", env, az),
41 "Type": pulumi.String("private"),
42 },
43 })
44 if err != nil {
45 return err
46 }
47 privateSubnetIDs = append(privateSubnetIDs, subnet.ID())
48 }
49 
50 // RDS Instance
51 db, err := rds.NewInstance(ctx, "main", &rds.InstanceArgs{
52 Engine: pulumi.String("postgres"),
53 EngineVersion: pulumi.String("16"),
54 InstanceClass: pulumi.String("db.r6g.large"),
55 AllocatedStorage: pulumi.Int(100),
56 StorageEncrypted: pulumi.Bool(true),
57 MultiAz: pulumi.Bool(env == "production"),
58 DeletionProtection: pulumi.Bool(env == "production"),
59 DbSubnetGroupName: dbSubnetGroup.Name,
60 VpcSecurityGroupIds: pulumi.StringArray{dbSG.ID()},
61 BackupRetentionPeriod: pulumi.Int(7),
62 })
63 if err != nil {
64 return err
65 }
66 
67 // Outputs
68 ctx.Export("vpcId", vpc.ID())
69 ctx.Export("dbEndpoint", db.Endpoint)
70 ctx.Export("privateSubnetIds", privateSubnetIDs)
71 
72 return nil
73 })
74}
75 

Reusable Components

Go's struct embedding and interfaces create composable infrastructure components:

go
1type VPCArgs struct {
2 Environment string
3 CidrBlock string
4 AZCount int
5}
6 
7type VPCOutput struct {
8 VpcID pulumi.StringOutput
9 PrivateSubnetIDs pulumi.StringArrayOutput
10 PublicSubnetIDs pulumi.StringArrayOutput
11}
12 
13func NewVPC(ctx *pulumi.Context, name string, args *VPCArgs) (*VPCOutput, error) {
14 vpc, err := ec2.NewVpc(ctx, name, &ec2.VpcArgs{
15 CidrBlock: pulumi.String(args.CidrBlock),
16 EnableDnsHostnames: pulumi.Bool(true),
17 EnableDnsSupport: pulumi.Bool(true),
18 Tags: defaultTags(args.Environment, name),
19 })
20 if err != nil {
21 return nil, err
22 }
23 
24 igw, err := ec2.NewInternetGateway(ctx, fmt.Sprintf("%s-igw", name), &ec2.InternetGatewayArgs{
25 VpcId: vpc.ID(),
26 Tags: defaultTags(args.Environment, fmt.Sprintf("%s-igw", name)),
27 })
28 if err != nil {
29 return nil, err
30 }
31 
32 var privateIDs, publicIDs pulumi.StringArray
33 azs := getAZs(args.AZCount)
34 
35 for i, az := range azs {
36 // Public subnet
37 pub, err := ec2.NewSubnet(ctx, fmt.Sprintf("%s-pub-%d", name, i), &ec2.SubnetArgs{
38 VpcId: vpc.ID(),
39 CidrBlock: pulumi.Sprintf("10.0.%d.0/24", i*2),
40 AvailabilityZone: pulumi.String(az),
41 MapPublicIpOnLaunch: pulumi.Bool(true),
42 })
43 if err != nil {
44 return nil, err
45 }
46 publicIDs = append(publicIDs, pub.ID())
47 
48 // Private subnet
49 priv, err := ec2.NewSubnet(ctx, fmt.Sprintf("%s-priv-%d", name, i), &ec2.SubnetArgs{
50 VpcId: vpc.ID(),
51 CidrBlock: pulumi.Sprintf("10.0.%d.0/24", i*2+1),
52 AvailabilityZone: pulumi.String(az),
53 })
54 if err != nil {
55 return nil, err
56 }
57 privateIDs = append(privateIDs, priv.ID())
58 }
59 
60 return &VPCOutput{
61 VpcID: vpc.ID(),
62 PrivateSubnetIDs: pulumi.ToStringArray(privateIDs).ToStringArrayOutput(),
63 PublicSubnetIDs: pulumi.ToStringArray(publicIDs).ToStringArrayOutput(),
64 }, nil
65}
66 

Testing Infrastructure Code

Go's testing framework works natively with Pulumi:

go
1func TestVPCConfiguration(t *testing.T) {
2 err := pulumi.RunErr(func(ctx *pulumi.Context) error {
3 vpc, err := NewVPC(ctx, "test", &VPCArgs{
4 Environment: "test",
5 CidrBlock: "10.0.0.0/16",
6 AZCount: 3,
7 })
8 if err != nil {
9 return err
10 }
11 
12 // Assert outputs
13 pulumi.All(vpc.VpcID, vpc.PrivateSubnetIDs).ApplyT(
14 func(args []interface{}) error {
15 vpcID := args[0].(string)
16 subnetIDs := args[1].([]string)
17 
18 assert.NotEmpty(t, vpcID)
19 assert.Len(t, subnetIDs, 3)
20 return nil
21 },
22 )
23 return nil
24 }, pulumi.WithMocks("project", "stack", &mockMonitor{}))
25 
26 assert.NoError(t, err)
27}
28 
29type mockMonitor struct{}
30 
31func (m *mockMonitor) NewResource(args pulumi.MockResourceArgs) (string, resource.PropertyMap, error) {
32 return args.Name + "-id", args.Inputs, nil
33}
34 
35func (m *mockMonitor) Call(args pulumi.MockCallArgs) (resource.PropertyMap, error) {
36 return args.Args, nil
37}
38 

Need a second opinion on your DevOps pipelines architecture?

I run free 30-minute strategy calls for engineering teams tackling this exact problem.

Book a Free Call

Custom Infrastructure Tools

Go excels at building custom infrastructure tooling:

go
1package main
2 
3import (
4 "context"
5 "fmt"
6 "log"
7 
8 "github.com/aws/aws-sdk-go-v2/config"
9 "github.com/aws/aws-sdk-go-v2/service/ec2"
10)
11 
12type ResourceAuditor struct {
13 ec2Client *ec2.Client
14}
15 
16func (a *ResourceAuditor) FindUntaggedResources(ctx context.Context) ([]string, error) {
17 instances, err := a.ec2Client.DescribeInstances(ctx, &ec2.DescribeInstancesInput{})
18 if err != nil {
19 return nil, err
20 }
21 
22 var untagged []string
23 for _, reservation := range instances.Reservations {
24 for _, instance := range reservation.Instances {
25 hasOwner := false
26 for _, tag := range instance.Tags {
27 if *tag.Key == "Owner" {
28 hasOwner = true
29 break
30 }
31 }
32 if !hasOwner {
33 untagged = append(untagged, *instance.InstanceId)
34 }
35 }
36 }
37 return untagged, nil
38}
39 

CDK for Terraform (CDKTF) with Go

go
1package main
2 
3import (
4 "github.com/aws/constructs-go/constructs/v10"
5 "github.com/hashicorp/terraform-cdk-go/cdktf"
6 aws "github.com/cdktf/cdktf-provider-aws-go/aws/v19"
7)
8 
9func NewMainStack(scope constructs.Construct, id string) cdktf.TerraformStack {
10 stack := cdktf.NewTerraformStack(scope, &id)
11 
12 aws.NewAwsProvider(stack, jsii.String("aws"), &aws.AwsProviderConfig{
13 Region: jsii.String("us-east-1"),
14 })
15 
16 vpc := aws.NewVpc(stack, jsii.String("vpc"), &aws.VpcConfig{
17 CidrBlock: jsii.String("10.0.0.0/16"),
18 EnableDnsHostnames: jsii.Bool(true),
19 })
20 
21 aws.NewSubnet(stack, jsii.String("subnet"), &aws.SubnetConfig{
22 VpcId: vpc.Id(),
23 CidrBlock: jsii.String("10.0.1.0/24"),
24 })
25 
26 return stack
27}
28 

Conclusion

Go's type system and compilation speed make it an effective language for Infrastructure as Code, particularly through Pulumi's Go SDK. The ability to write real loops, conditionals, and abstractions using Go's standard patterns eliminates the awkwardness of HCL for complex infrastructure topologies. Go's testing framework enables unit testing infrastructure code — something Terraform's native testing support is still maturing on.

The trade-off is ecosystem size. Terraform's HCL has vastly more community examples, blog posts, and Stack Overflow answers. For standard infrastructure patterns (VPC + ECS + RDS), Terraform is faster to get started. For complex patterns (dynamic resource counts, conditional deployments, custom infrastructure tooling), Go with Pulumi provides more power and maintainability.

FAQ

Need expert help?

Building with CI/CD pipelines?

I help teams ship production-grade systems. From architecture review to hands-on builds.

Muneer Puthiya Purayil

SaaS Architect & AI Systems Engineer. 10+ years shipping production infrastructure across fintech, automotive, e-commerce, and healthcare.

Engage

Start a
Conversation.

For teams building at scale: SaaS platforms, agentic AI systems, and enterprise mobile infrastructure. Scope and fit are evaluated before any engagement begins.

Limited availability · Q3 / Q4 2026