Practicing AWS IaC patterns without a cloud bill. Ministack runs AWS APIs locally, MinIO holds Terraform state, Ansible provisions the homelab, Terragrunt handles multi-environment layouts. One command up, one command down.
The repo: github.com/75asu/platform-zero
Stack
- Ministack — AWS simulator (DynamoDB, SQS, SNS, S3) on port 4566. In-memory, wiped on container stop.
- MinIO — S3-compatible store on port 9000. Holds Terraform remote state.
- Ansible — provisions docker-compose to the homelab over SSH.
- Terragrunt — multi-environment IaC.
dev/andstaging/with separate state and provider config.
Workflow
make deploy # homelab up + all terraform environments applied
make teardown # docker compose down, clean slate
Both Ministack and MinIO are ephemeral by design — teardown is just stopping containers, no terraform destroy needed.
Structure
New services get added as modules. Terragrunt picks them up automatically on the next deploy:
terraform/live/
├── dev/
│ ├── account.hcl
│ ├── s3/
│ ├── iam/ ← next
│ ├── sqs/ ← planned
│ └── dynamodb/ ← planned
└── staging/
└── ... mirrors dev
Each module is standard Terraform. The root config handles provider endpoints and state backend — adding a module doesn’t touch either.
Multi-Environment
Each environment sets its own endpoint and access key in account.hcl. Ministack namespaces resources by access key, so dev and staging don’t collide on the same container:
locals {
environment = "dev"
aws_endpoint = get_env("AWS_ENDPOINT_URL", "http://localhost:4566")
access_key = "dev-key"
}
State keys include the path:
key = "live/${path_relative_to_include()}/terraform.tfstate"
# live/dev/s3/terraform.tfstate
# live/staging/s3/terraform.tfstate
Split Backend
State is in MinIO, locking is in Ministack DynamoDB. Ministack has a bug with S3 Public Access Block deletions that hangs terraform destroy. MinIO doesn’t. Terragrunt supports separate endpoints for each:
remote_state {
backend = "s3"
config = {
endpoint = local.minio_endpoint # MinIO
dynamodb_endpoint = local.aws_endpoint # Ministack
dynamodb_table = "platform-zero-tfstate-lock"
}
}
Setup Notes
A few things worth knowing if you’re building something similar:
- MinIO requires an 8-char minimum password.
testfails silently withFATAL: Invalid credentials. Useminioadminor anything longer. - Terragrunt 1.0 changed the CLI.
run-all applyis nowrun --all -- apply. AddTG_NON_INTERACTIVE=truefor unattended runs. - Don’t
sourceinside Makefile recipes. Export env vars at the Makefile level instead — every recipe line inherits them. - Use
docker waitfor init containers. Rather than polling HTTP endpoints from Ansible, a one-shotminio/mccontainer creates the state bucket on startup.docker wait awslab-minio-initis the authoritative readiness check.
What’s Next
S3 modules are done in both environments. IAM is next — roles, policies, trust relationships. After that, SQS and application DynamoDB tables.
The goal is to have provisioned each service, broken it, and rebuilt it before touching it in a real environment.