The Philosophical Developer -- Chapter 28: The Modules

2026-07-21 · 4 min read

Pulumi Go Port - Chapter 28: The Modules

Chapter 27 established the pattern. A single module, a test, a CI step. It was enough to prove the methodology transferred. But one module is not a platform. The local-cloud infrastructure has seven modules, and each one has its own resource types, its own configuration surface, and its own test file.

I sat down with OrsonRius and we ported the remaining six modules. One by one. Test first. Red, green, refactor.

The network module

The network module provisions a VPC with a public subnet. It is the foundation everything else depends on. The test validates that the VPC and subnet are created with the correct CIDR blocks.

The Go code is straightforward. Two resources, one for the VPC and one for the subnet. The subnet references the VPC by ID. The outputs export the identifiers for other modules to use.

func createNetwork(ctx *pulumi.Context) error {
    vpc, err := ec2.NewVpc(ctx, "local-cloud-vpc", &ec2.VpcArgs{
        CidrBlock:          pulumi.String("10.0.0.0/16"),
        EnableDnsSupport:   pulumi.Bool(true),
        EnableDnsHostnames: pulumi.Bool(true),
        Tags: pulumi.StringMap{
            "Name":        pulumi.String("local-cloud-vpc"),
            "managed_by":  pulumi.String("pulumi"),
            "environment": pulumi.String("local"),
        },
    })
    if err != nil {
        return err
    }

    subnet, err := ec2.NewSubnet(ctx, "local-cloud-public-subnet", &ec2.SubnetArgs{
        VpcId:            vpc.ID(),
        CidrBlock:        pulumi.String("10.0.1.0/24"),
        AvailabilityZone: pulumi.String("us-east-1a"),
        Tags: pulumi.StringMap{
            "Name":        pulumi.String("local-cloud-public"),
            "managed_by":  pulumi.String("pulumi"),
            "environment": pulumi.String("local"),
        },
    })
    if err != nil {
        return err
    }

    ctx.Export("vpcId", vpc.ID())
    ctx.Export("subnetId", subnet.ID())
    return nil
}

The IAM module

IAM is the most fiddly module in any infrastructure project. The Pulumi version takes the same approach as the OpenTofu original: an inline JSON policy document, a role, a policy, and a role-policy attachment.

The role defines the trust policy for ECS tasks. The policy grants S3 access and CloudWatch logging. The attachment binds them together.

func createIAM(ctx *pulumi.Context) error {
    assumeRoleJSON, _ := json.Marshal(map[string]interface{}{
        "Version": "2012-10-17",
        "Statement": []map[string]interface{}{
            {"Effect": "Allow",
             "Principal": map[string]interface{}{"Service": "ecs-tasks.amazonaws.com"},
             "Action":    "sts:AssumeRole"},
        },
    })

    role, _ := iam.NewRole(ctx, "local-cloud-role", &iam.RoleArgs{
        Name:             pulumi.String("local-cloud-role"),
        AssumeRolePolicy: pulumi.String(string(assumeRoleJSON)),
    })
    // ...
}

The DynamoDB module

The state table is a single DynamoDB table with PAY_PER_REQUEST billing and a string hash key. It is the simplest module in the stack.

func createDynamoDB(ctx *pulumi.Context) error {
    table, _ := dynamodb.NewTable(ctx, "local-cloud-state", &dynamodb.TableArgs{
        Name:        pulumi.String("local-cloud-state"),
        BillingMode: pulumi.String("PAY_PER_REQUEST"),
        HashKey:     pulumi.String("id"),
        Attributes: dynamodb.TableAttributeArray{
            &dynamodb.TableAttributeArgs{
                Name: pulumi.String("id"),
                Type: pulumi.String("S"),
            },
        },
    })
    ctx.Export("tableName", table.Name)
    return nil
}

The ECS module

The ECS module is the most complex. It creates a cluster, a task definition with a Fargate container, and a service that runs the container. The task definition includes a JSON container definition, a CPU and memory allocation, and references to the IAM role.

The service wires everything together: the cluster, the task definition, the network configuration, and the desired count.

The ECR module

The container registry module is a single ECR repository with image scanning enabled. It is the entry point for the container image pipeline.

func createECR(ctx *pulumi.Context) error {
    repo, _ := ecr.NewRepository(ctx, "local-cloud-ecr", &ecr.RepositoryArgs{
        Name: pulumi.String("hello-local-cloud"),
        ImageScanningConfiguration: &ecr.RepositoryImageScanningConfigurationArgs{
            ScanOnPush: pulumi.Bool(true),
        },
    })
    ctx.Export("repositoryUrl", repo.RepositoryUrl)
    return nil
}

The K8s module

The Kubernetes module is the only one that uses the Pulumi Kubernetes SDK instead of the AWS SDK. It creates a namespace, a deployment, and a ClusterIP service. The deployment runs an nginx container with two replicas.

The K8s SDK is structurally different from the AWS SDK. Resources use the Kubernetes API conventions rather than the AWS provider conventions. The ObjectMetaArgs pattern is consistent across all K8s resources.

The full stack test

The final test composes all seven modules and verifies the stack runs without errors. It is the integration test for the module level.

func TestLocalCloudStack(t *testing.T) {
    err := pulumi.RunErr(func(ctx *pulumi.Context) error {
        for _, fn := range []func(*pulumi.Context) error{
            createStorage, createNetwork, createIAM,
            createDynamoDB, createECS, createECR, createK8s,
        } {
            if err := fn(ctx); err != nil {
                return err
            }
        }
        return nil
    }, pulumi.WithMocks("project", "stack", mocks(0)))
    assert.NoError(t, err)
}

The results

All eight tests pass. The build compiles cleanly. The CI pipeline runs on every push.

=== RUN   TestLocalCloudStack    --- PASS
=== RUN   TestStorageModule      --- PASS
=== RUN   TestNetworkModule      --- PASS
=== RUN   TestIAMModule          --- PASS
=== RUN   TestDynamoDBModule     --- PASS
=== RUN   TestECSModule          --- PASS
=== RUN   TestECRModule          --- PASS
=== RUN   TestK8sModule          --- PASS
PASS    ok  local-cloud-pulumi   0.022s

The average test execution time is 2.75 milliseconds per test. The entire test suite runs in under 25 milliseconds. Compare that to a Terraform plan that takes 30 seconds per module and you start to see the appeal of the mock-driven approach.

Twenty-two milliseconds. Eight modules. Zero infrastructure.

Repos:

LinkedIn:

Here I geek out with my young Padawan, OrsonRius.