The Philosophical Developer — Chapter 27: Why Pulumi + Go
2026-07-21 · 5 min read

The OpenTofu arc ran from Chapter 13 to Chapter 20. Seven modules across three clouds, each one written in HCL, each one tested with command = plan, each one orchestrated through a Dagger pipeline. It worked. It proved that infrastructure-as-code could be testable, modular, and portable across providers.
But I had a question that kept nagging at me.
Was the methodology provider-independent, or was it HCL-specific? Would the same test-first, mock-driven approach work in a general-purpose programming language? Could we prove that the discipline held regardless of the tool?
I sat down with my Padawan, OrsonRius, and we designed an experiment. Port the entire local-cloud infrastructure to Pulumi using Go. Same modules. Same test patterns. Same CI pipeline. Different language, different runtime, different provider SDK.
Why Pulumi
Pulumi is not Terraform with a different syntax. It is a fundamentally different approach to infrastructure as code.
HCL is a declarative configuration language. You describe what you want and the tool figures out how to get there. It is good at what it does. But it is also limited. You cannot import libraries. You cannot write loops that depend on runtime values. You cannot use the type system to catch mistakes before the plan runs.
Pulumi replaces the configuration language with a real programming language. You write infrastructure in Go, Python, TypeScript, or any of the supported languages. The provider SDK is a library. The state engine is a runtime. The plan is still a plan, but it is constructed from programmatic primitives.
Go was the natural choice for us. It is the language of the local-cloud infrastructure toolchain. The Dagger pipeline is written in Go. The Floci emulator is Go-adjacent. Using Go for Pulumi meant we stayed in the same ecosystem, the same tooling, the same build system.
The module structure
The AWS local-cloud has seven modules. Each one maps to a Terraform module from the original arc. The Pulumi port mirrors the structure exactly.
| Module | What It Creates | OpenTofu Equivalent |
|---|---|---|
| Storage | Versioned S3 bucket | modules/storage |
| Network | VPC + public subnet | modules/network |
| IAM | ECS task role + policy | modules/iam |
| DynamoDB | State table | modules/dynamodb |
| ECS | Fargate cluster + service | modules/ecs |
| ECR | Container registry | modules/ecr |
| K8s | Namespace + deployment + service | modules/k8s |
Each module is a single Go file in the infrastructure/pulumi/ directory. No subdirectories, no component resources, no abstraction layers. Just functions that create resources and export outputs. The main function composes them in order.
The test pattern
The key innovation in the OpenTofu arc was testable infrastructure. Each module had a tftest.hcl file that ran command = plan and asserted on the outputs. No real infrastructure required. No cloud credentials. No state files.
Pulumi supports the same pattern through mocks. The pulumi.WithMocks option replaces the real provider with a mock that returns controlled responses. The test runs the same Go code that would run in production, but the resource creation calls are intercepted and return fake data.
The test file has four parts. A mocks struct that implements two methods. NewResource returns the resource ID and any computed properties. Call returns mock data for function calls. The test function wraps the infrastructure code in pulumi.RunErr with the mocks attached.
type mocks int
func (mocks) NewResource(args pulumi.MockResourceArgs) (string, resource.PropertyMap, error) {
outputs := args.Inputs.Mappable()
switch args.TypeToken {
case "aws:s3/bucket:Bucket":
outputs["arn"] = "arn:aws:s3:::local-cloud-artifacts"
outputs["bucket"] = "local-cloud-artifacts"
}
return args.Name + "_id", resource.NewPropertyMapFromMap(outputs), nil
}
func TestStorageModule(t *testing.T) {
err := pulumi.RunErr(func(ctx *pulumi.Context) error {
return createStorage(ctx)
}, pulumi.WithMocks("project", "stack", mocks(0)))
assert.NoError(t, err)
}
The test runs in milliseconds. No network calls. No infrastructure. Just the Go code, the mock, and the assertion. It is the same discipline as the OpenTofu tests, but expressed in Go.
The storage module
The storage module was the first one we ported. It is the simplest module and the best place to establish the pattern.
func createStorage(ctx *pulumi.Context) error {
bucket, err := s3.NewBucket(ctx, "local-cloud-storage", &s3.BucketArgs{
Bucket: pulumi.String("local-cloud-artifacts"),
Tags: pulumi.StringMap{
"managed_by": pulumi.String("pulumi"),
"environment": pulumi.String("local"),
},
})
if err != nil {
return err
}
_, err = s3.NewBucketVersioning(ctx, "local-cloud-storage-versioning",
&s3.BucketVersioningArgs{
Bucket: bucket.Bucket,
VersioningConfiguration: &s3.BucketVersioningVersioningConfigurationArgs{
Status: pulumi.String("Enabled"),
},
})
if err != nil {
return err
}
ctx.Export("bucketName", bucket.Bucket)
ctx.Export("bucketArn", bucket.Arn)
ctx.Export("versioningEnabled", pulumi.String("Enabled"))
return nil
}
The pattern is identical across all modules. Create the resource. Check the error. Export the output. The Pulumi SDK handles the rest.
What we learned
The first lesson was that the methodology transfers cleanly. The test-first, mock-driven approach that worked for OpenTofu works equally well for Pulumi. The discipline is tool-independent. It is about how you write infrastructure, not what syntax you write it in.
The second lesson was that Go adds compiler-level safety. HCL catches missing attributes at plan time. Go catches them at compile time. A misspelled field name, a wrong type, a missing required argument – all caught before the code even runs. That alone is worth the migration.
The third lesson was that the Pulumi SDK is different enough from the Terraform provider that you cannot just transliterate. The S3 bucket versioning resource, for example, is a separate resource in the Terraform AWS provider but a field on the bucket arguments in Pulumi. The IAM policy document is a JSON string in Pulumi, not a separate data source. These differences matter. They are not hard to adapt to, but they require attention.
The code is in the repo. The tests pass. The CI pipeline is live. The next chapter ports the remaining six modules.
Repos:
- github.com/dark5un/local-cloud – infrastructure/pulumi/
LinkedIn:
Here I geek out with my young Padawan, OrsonRius.