The Philosophical Developer — Chapter 18: Image Management & ECR

2026-07-10 · 4 min read

Image Management & ECR

Chapter 14 left a debt. I built six OpenTofu modules: storage, network, IAM, DynamoDB, ECS, and k8s. But one piece was missing. The ECR module timed out against Floci during that session, and I moved on instead of fixing it. The module list had a hole, and every time I looked at the repository I felt it.

Code your infrastructure means all of it. Not most of it. Not six out of seven. Every service that has an AWS provider should be represented, tested, and verified in plan mode. So this chapter closes that hole.

The Missing Piece

ECR is the Elastic Container Registry. In the local cloud, it is the place where container images live before they are deployed. Without it, the image lifecycle is ad hoc. You build a Docker image, push it to the local registry on port 5000, and import it into k3d by hand. That works for a demo. It is not infrastructure.

A proper ECR module gives you:

  • A named repository with a predictable URL
  • Image tag mutability control (MUTABLE for dev, IMMUTABLE for production)
  • Scan-on-push for vulnerability detection
  • Integration with the Dagger pipeline so the registry is validated before any deploy

The Structure

The module follows the same pattern as every other module in the repository. One main.tf, one ecr.tftest.hcl with four test cases in plan mode. The resource is aws_ecr_repository with image_scanning_configuration and image_tag_mutability as configurable variables.

resource "aws_ecr_repository" "this" {
  name = local.full_name

  image_tag_mutability = var.image_tag_mutability

  image_scanning_configuration {
    scan_on_push = var.scan_on_push
  }

  tags = merge(var.tags, {
    managed_by  = "opentofu"
    environment = "local"
  })
}

The naming convention appends -local-cloud to the repository name automatically. hello becomes hello-local-cloud. This prevents name collisions if you have multiple projects on the same registry.

The Tests

Four test cases, all running in command = plan mode for idempotency:

  1. Default repository with default variables. Assert the name, ARN, and registry ID are populated.
  2. Custom repository name with a different repository_name. Assert the convention holds.
  3. Immutable tags with image_tag_mutability = "IMMUTABLE" and scan_on_push = false. Assert the name is consistent.
  4. Integration plan in the local environment, verifying the ECR output is present alongside the other six modules.
run "default_repository" {
  command = plan

  module {
    source = "./."
  }

  assert {
    condition     = output.repository_name == "hello-local-cloud"
    error_message = "Repository name should use full_name convention"
  }
}

Note: repository_url is computed (null in plan mode) because the AWS provider cannot resolve the URL without an actual API call. This is a known limitation of plan-mode testing. The ARN and registry ID are static and validate fine.

Pipeline Integration

The ECR module is picked up automatically by the Dagger pipeline. The validate-modules and test-modules stages iterate over every directory in modules/, so adding a new module is zero-config. The integration-plan stage now shows 12 resources to add instead of 11:

Plan: 12 to add, 0 to change, 0 to destroy.

Changes to Outputs:
  + bucket_name        = "local-cloud-artifacts"
  + ecr_repository     = "hello-local-cloud"
  + ecr_repository_url = (known after apply)

The local environment’s main.tf now includes the ECR module with the ecr endpoint pointed at Floci, and the local.tftest.hcl asserts the repository name matches convention.

Why This Matters

This is not about a single module. It is about the discipline of completeness. When you leave a hole, you carry a cognitive debt. Every time you look at the directory listing and see six modules instead of seven, your brain registers the gap. Over time, those gaps accumulate. You start accepting them. “We will get to it later.” Later becomes never.

The four pillars apply here:

  • Reproducible: the module produces the same repository every time, with the same configuration, from the same code.
  • Traceable: every change to the module is tracked in git and validated by the pipeline.
  • Testable: plan-mode tests verify the module’s contract without side effects.
  • Safe: no repository is created without a plan review. No tags are mutable without explicit configuration.

What Comes Next

The ECR module is the foundation for the next chapter. Once images have a proper home in the registry, the pipeline can automate the build/push/deploy cycle. That is Chapter 19: Pipeline as Test Suite, where I will write integration tests that actually deploy a container to Floci, verify it is running, and tear it down.

The pattern is emerging. Every chapter closes a gap and opens the next possibility. The local cloud is not a destination. It is a process.


Repos: github.com/dark5un/local-cloud

Also on LinkedIn.

Here I geek out with my young Padawan, OrsonRius.