The Philosophical Developer — Getting Started with the Local Cloud

2026-07-10 · 8 min read

Getting Started with the Local Cloud

This series has covered a lot of ground. Twenty chapters, three cloud providers, two programming languages, and one methodology that has held across all of them.

But if you are joining us here, you might wonder: where do I start? What do I actually install? Do I need a cloud account? Do I need Kubernetes?

The answer to the last two is no. You need Docker and a terminal.

This guide walks through four scenarios that cover the full methodology. Each scenario builds on the last. By the end, you will have the entire local cloud stack running on your machine, with tested infrastructure code that works against any cloud provider.

What You Need

  • Docker (24.0+) — for running Floci emulators
  • OpenTofu 1.12+ — tofu CLI, latest from opentofu.org
  • Git — for version control
  • A terminal — Linux, macOS, or Windows (WSL2)

Optional but recommended:

  • Go 1.24+ — if you want to build the Dagger pipeline locally
  • Dagger CLI — dagger binary, for running the CI pipeline

No cloud accounts. No credit cards. No auth tokens.

The Four Pillars

Everything in this series rests on these four principles:

  • Reproducible — same inputs, same outputs, every time
  • Traceable — every change is in git, every run is logged
  • Testable — tests gate every change, plan mode for idempotency
  • Safe — nothing applies automatically, human review required

If a decision does not serve at least one of these, it is not the right decision.

Scenario 1: Deploy a Storage Bucket on AWS

This is the hello world of infrastructure. One S3 bucket, one command, one emulator.

Step 1: Start Floci

Floci is the AWS emulator. It runs on port 4566 and provides 69 AWS services out of the box.

docker run --rm -d --name floci -p 4566:4566 floci/floci:latest

That is it. No settings, no configuration, no cloud account. The emulator is ready in milliseconds.

Step 2: Write the Module

Create a directory for your infrastructure:

mkdir -p my-local-cloud/infrastructure/opentofu/modules/storage
cd my-local-cloud

Create infrastructure/opentofu/modules/storage/main.tf:

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = ">= 5.0"
    }
  }
}

variable "bucket_name" {
  type    = string
  default = "my-first-bucket"
}

resource "aws_s3_bucket" "this" {
  bucket = var.bucket_name
}

output "bucket_name" {
  value = aws_s3_bucket.this.bucket
}

Step 3: Write the Test

Create infrastructure/opentofu/modules/storage/storage.tftest.hcl:

provider "aws" {
  region                      = "us-east-1"
  access_key                  = "test"
  secret_key                  = "test"
  skip_credentials_validation = true
  skip_metadata_api_check     = true
  skip_requesting_account_id  = true

  endpoints {
    s3 = "http://localhost:4566"
  }
}

run "create_bucket" {
  command = plan

  module {
    source = "./."
  }

  assert {
    condition     = output.bucket_name == "my-first-bucket"
    error_message = "Bucket name should match default"
  }
}

Step 4: Run the Test

cd infrastructure/opentofu/modules/storage
tofu init -backend=false
tofu test

You should see:

Success! 1 passed, 0 failed.

That single test validates that your module structure is correct, your variable contracts are sound, and your outputs are well-typed. No S3 bucket was created. Floci was not contacted. The test ran entirely in plan mode.

Step 5: Apply Against Floci

cd infrastructure/opentofu/modules/storage
tofu init
tofu plan
tofu apply -auto-approve

Now check that the bucket actually exists:

curl http://localhost:4566/_localstack/health | grep s3

You just deployed infrastructure to a local AWS emulator. No cloud account. No cost. No delay.

Scenario 2: Test-Driven Infrastructure

The hello world proved the tooling works. This scenario proves the methodology works.

The rule is simple: write the test before the module. Red. Green. Refactor. Same as TDD for application code.

Step 1: Write the Test (RED)

Create infrastructure/opentofu/modules/database/database.tftest.hcl:

provider "aws" {
  region                      = "us-east-1"
  access_key                  = "test"
  secret_key                  = "test"
  skip_credentials_validation = true
  skip_metadata_api_check     = true
  skip_requesting_account_id  = true

  endpoints {
    dynamodb = "http://localhost:4566"
  }
}

run "create_table" {
  command = plan

  module {
    source = "./."
  }

  assert {
    condition     = output.table_name == "my-table"
    error_message = "Table name should match default"
  }

  assert {
    condition     = output.billing_mode == "PAY_PER_REQUEST"
    error_message = "Default billing should be pay-per-request"
  }
}

Run it:

cd infrastructure/opentofu/modules/database
tofu init -backend=false
tofu test

It fails. The module does not exist yet. This is the RED phase.

Step 2: Write the Module (GREEN)

Create main.tf:

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = ">= 5.0"
    }
  }
}

variable "table_name" {
  type    = string
  default = "my-table"
}

resource "aws_dynamodb_table" "this" {
  name         = var.table_name
  billing_mode = "PAY_PER_REQUEST"
  hash_key     = "id"

  attribute {
    name = "id"
    type = "S"
  }
}

output "table_name" {
  value = aws_dynamodb_table.this.name
}

output "billing_mode" {
  value = aws_dynamodb_table.this.billing_mode
}

Run the test again:

tofu test

Green. The module now satisfies the contract defined by the test.

Step 3: Refactor

Add tags, change defaults, add more outputs. Run the test suite after every change. The test tells you immediately if you broke something.

This is the pattern for every module in the series. The test defines the contract. The module implements it. The pipeline enforces it.

Scenario 3: Run the Dagger Pipeline

A single module is a component. A pipeline is a system.

The Dagger pipeline runs four stages, each in an isolated container:

  1. fmt-check — enforces consistent formatting across all modules
  2. validate-modules — initialises and validates each module
  3. test-modules — runs every plan-mode test
  4. integration-plan — generates a combined plan for the full environment

Step 1: Create the Pipeline

Create pipeline/main.go:

package main

import (
    "context"
    "dagger/pipeline/internal/dagger"
)

type Pipeline struct{}

func (m *Pipeline) infraDir() *dagger.Directory {
    return dag.CurrentModule().Source().Directory("infrastructure/opentofu")
}

func (m *Pipeline) tofuContainer() *dagger.Container {
    return dag.Container().
        From("debian:latest").
        WithExec([]string{"apt-get", "update"}).
        WithExec([]string{"apt-get", "install", "-y", "wget", "ca-certificates", "tar", "gzip"}).
        WithExec([]string{"wget", "-O", "/tmp/tofu.tar.gz",
            "https://github.com/opentofu/opentofu/releases/download/v1.12.1/tofu_1.12.1_linux_amd64.tar.gz"}).
        WithExec([]string{"tar", "-xzf", "/tmp/tofu.tar.gz", "-C", "/usr/local/bin"}).
        WithExec([]string{"rm", "/tmp/tofu.tar.gz"})
}

func (m *Pipeline) ValidateModules(ctx context.Context) (string, error) {
    cmd := "for mod in modules/*/; do cd \"$mod\" && tofu init -backend=false && tofu validate; cd -; done"
    return m.tofuContainer().
        WithDirectory("/infra", m.infraDir()).
        WithWorkdir("/infra").
        WithExec([]string{"sh", "-c", cmd}).
        Stdout(ctx)
}

Step 2: Run the Pipeline

cd pipeline
dagger init --sdk=go
dagger develop
dagger call validate-modules

The pipeline downloads OpenTofu, initialises every module, and validates them all in an isolated container. No local dependencies, no version conflicts, no “works on my machine.”

Scenario 4: Deploy to a Second Cloud

The methodology is not tied to AWS. The same discipline applies to GCP and Azure.

To switch clouds, you change three things:

  1. The provider block — from hashicorp/aws to hashicorp/google or hashicorp/azurerm
  2. The emulator — from floci/floci:latest (AWS, port 4566) to floci/floci-gcp:latest (GCP, port 4588) to floci/floci-az:latest (Azure, port 4577)
  3. The resource types — from aws_s3_bucket to google_storage_bucket to azurerm_storage_account

Everything else stays the same. The test structure, the pipeline stages, the TDD cycle, the four pillars. The cloud provider is a parameter. The methodology is the constant.

Here is the same scenario from Scenario 1, but for GCP:

provider "google" {
  project     = "my-project"
  region      = "us-central1"
  access_token = "test"

  storage_custom_endpoint = "http://localhost:4588/storage/v1/"
}

resource "google_storage_bucket" "this" {
  name     = "my-first-bucket"
  location = "US"
}

One port change, one provider swap, one resource rename. The same test pattern. The same pipeline. The same result.

Putting It All Together

The complete local cloud workflow fits in six commands:

# 1. Start the emulator
docker run --rm -d --name floci -p 4566:4566 floci/floci:latest

# 2. Write a module (with a test first)
vim infrastructure/opentofu/modules/my-module/main.tf

# 3. Write the test
vim infrastructure/opentofu/modules/my-module/my-module.tftest.hcl

# 4. Test in plan mode
cd infrastructure/opentofu/modules/my-module
tofu init -backend=false && tofu test

# 5. Run the full pipeline
cd pipeline
dagger call test-modules

# 6. Commit
git add -A && git commit -m "feat: my-module with plan-mode tests"

No cloud console. No waiting for resources. No surprise bills. Infrastructure as code, tested and validated, on your machine.

The Philosophy

The local cloud is not about running everything locally forever. It is about knowing your infrastructure works before you trust it with production.

A module that passes tofu test in plan mode is guaranteed to have the right structure. A module that passes dagger call validate-modules is guaranteed to compose correctly with its neighbours. A plan reviewed before apply is guaranteed to have human oversight.

The four pillars are not abstract ideals. They are checkpoints in a workflow:

  • Reproducible — the emulator produces the same state every time
  • Traceable — every change is in git, every run is logged by Dagger
  • Testable — plan-mode tests run in seconds without side effects
  • Safe — nothing reaches production without a reviewed plan

Start with one bucket. Add a database module. Add a pipeline. Add a second cloud. The methodology scales with you.


Repos:

Here I geek out with my young Padawan, OrsonRius.