E L E M E N T A L I N F R A S T R U C T U R E F R A M E W O R K
Build infrastructure the way nature builds matter — atom by atom.
Philosophy · Model · Providers · Structure · Renderer · Environments · Versioning · State · Usage · Roadmap
Modern cloud infrastructure suffers from two opposite extremes: the monolith temptation — a single Terraform repository that holds everything — and the chaotic fragmentation of disconnected modules without cohesion.
EIF proposes a third way, inspired by chemistry.
Every infrastructure resource has its own atomic identity, composable with precision into increasingly complex structures, up to fully deployable applications. The model is simple, the naming is intentional, the hierarchy is strict.
EIF organizes Terraform code into three hierarchical levels of abstraction:
Internal. Not user-facing.
A single cloud service written in plain HCL. Atoms are the primitive building blocks of the framework — scoped to one service only. They are composed by molecules and are never deployed directly.
Atoms are namespaced by cloud provider: atoms/aws/, atoms/azure/, atoms/gcp/.
Internal. Not user-facing.
A combination of atoms forming a coherent architectural pattern. Intra-molecule atom dependencies are wired explicitly via output references — each atom that depends on another consumes its output directly. Molecules are never deployed directly.
Molecules are namespaced by cloud provider: molecules/aws/, molecules/azure/, molecules/gcp/.
| Molecule | Cloud | Atoms | Dependency chain |
|---|---|---|---|
single-page-application |
AWS | s3 + cloudfront + waf |
cloudfront ← s3.domain, waf.arn |
db |
AWS | rds + sg |
sg port derived from engine → rds ← sg.id |
lambda-svc |
AWS | lambda + sg |
lambda ← sg.id |
single-page-application |
Azure | blob + frontdoor |
frontdoor ← blob.primary_web_endpoint |
single-page-application |
GCP | gcs + cdn + armor |
cdn ← gcs.bucket_name, armor.id |
The only user-facing level.
Matter is the sole entry point for every deployment. Even a deployment using a single molecule is expressed as a matter. Each matter has two types of input file:
composition.json— stable structure: which molecules to include and which version to pin. Shared across all environments, only changes when the architecture changes.<env>.json— a flat pool of variables for that environment. No per-molecule grouping, noenvironmentkey — that is injected automatically by the renderer from the CLI argument.
// composition.json — structure (environment-agnostic)
{
"matter": "three-tier-app",
"molecules": [
{ "name": "single-page-application", "source": "molecules/aws/single-page-application/v1" },
{ "name": "db", "source": "molecules/aws/db/v1" },
{ "name": "lambda-svc", "source": "molecules/aws/lambda-svc/v1" }
]
}// prod.json — flat variable pool for production
{
"account": "prod",
"bucket_name": "my-app-assets-prod",
"s3_versioning_enabled": true,
"cloudfront_price_class": "PriceClass_100",
"instance_class": "db.t3.medium",
"multi_az": true,
"vpc_id": "vpc-prod-id",
"subnet_ids": ["subnet-a", "subnet-b"],
"memory_mb": 512,
"timeout_s": 30
}The eif renderer injects environment automatically and makes all flat vars available to the Jinja2 template. The template is the wiring layer — it explicitly maps flat variables to each module's inputs, using {{ src['mol-name'] }} to reference the pinned source path.
Providers are fully abstracted from the core framework. Each cloud provider lives in its own directory:
providers/
aws/
provider.tf.j2 ← terraform{} + provider "aws" block
azure/
provider.tf.j2 ← terraform{} + provider "azurerm" block
gcp/
provider.tf.j2 ← terraform{} + provider "google" block
Each provider.tf.j2 receives the account config as Jinja2 context and renders the full terraform {} + provider {} block. The rendered output is injected into the matter template as {{ provider_block }}.
Adding a new provider requires no changes to eif.py or any existing files — just create providers/<cloud>/provider.tf.j2.
The accounts.json entry for each environment declares which provider to use:
{
"dev": { "provider": "aws", "aws_region": "us-east-1", "profile": "eif-dev" },
"azure-dev": { "provider": "azure", "subscription_id": "...", "tenant_id": "..." },
"gcp-prod": { "provider": "gcp", "project": "my-project", "region": "us-central1" }
}This repository contains the CLI tool only. The component library lives in eif-library.
eif/ ← this repo — the CLI tool
├── eif.py
├── pyproject.toml
└── examples/ ← reference implementation
├── accounts.example.json
├── providers/
├── atoms/
├── molecules/
└── matters/
When using EIF in practice, your library repo follows this layout:
your-eif-library/
│
├── accounts.json # env → cloud account config
│
├── providers/ # Cloud provider templates (pluggable)
│ ├── aws/
│ │ ├── provider.tf.j2 # terraform{} + provider "aws"
│ │ └── backend.tf.j2 # S3 backend block
│ ├── azure/
│ │ ├── provider.tf.j2
│ │ └── backend.tf.j2
│ └── gcp/
│ ├── provider.tf.j2
│ └── backend.tf.j2
│
├── atoms/ # Atomic cloud services (plain HCL)
│ └── <cloud>/<category>/<name>/v1/ # main.tf · variables.tf · outputs.tf
│
├── molecules/ # Architectural blueprints
│ └── <cloud>/<name>/v1/
│
└── matters/ # Deployable applications
└── <name>/<cloud>/
├── composition.json # molecule list + pinned versions
├── <env>.json # flat variable pool per environment
└── main.tf.j2 # wiring template
The eif CLI takes a matter directory and an environment name. It:
- Loads
accounts.jsonfrom the repo root - Loads
composition.jsonfrom the matter directory - Loads
<env>.jsonfrom the matter directory - Renders
providers/<cloud>/provider.tf.j2→provider_block - Builds a
srcdict mapping each molecule name to its resolved source path - Merges account config, flat env vars, and
environmentinto the template context - Renders
main.tf.j2→.rendered/<env>/main.tf
accounts.json ──┐
providers/<cloud>/*.tf.j2 ┤
composition.json ──┼──▶ eif ──▶ .rendered/<env>/main.tf
<env>.json ──┤
main.tf.j2 ──┘
Each environment maps to a cloud account defined in accounts.json. The provider field determines which providers/<cloud>/provider.tf.j2 is used:
{
"dev": { "provider": "aws", "aws_region": "us-east-1", "profile": "eif-dev" },
"test": { "provider": "aws", "aws_region": "us-east-1", "profile": "eif-test" },
"prod": { "provider": "aws", "aws_region": "us-east-1", "assume_role_arn": "arn:aws:iam::ACCOUNT_ID:role/EIFDeployRole" }
}devandtestauthenticate via named AWS CLI profilesproddeploys to a separate AWS account via role assumption- Azure and GCP accounts follow the same pattern with their own auth fields
Atoms and molecules are versioned via subdirectories (v1/, v2/, ...). This guarantees that matter already in production is never broken by new feature work.
The rule:
| Change type | Action |
|---|---|
| Bug fix, new optional variable | Edit in place within the existing version |
| Breaking change (remove var, change type, restructure outputs) | Create a new version directory alongside the old one |
Example — adding a breaking change to an atom:
atoms/aws/storage/s3/
v1/ ← existing production matter stays pinned here
v2/ ← new interface; new molecules reference this
The molecule that needs the new feature gets its own v2/ referencing atoms/aws/storage/s3/v2. All existing matter compositions continue to pin molecules/aws/single-page-application/v1 and are completely unaffected.
Molecule sources are pinned in composition.json:
{ "name": "single-page-application", "source": "molecules/aws/single-page-application/v1" }To bump all pinned molecule versions in composition.json to the latest available:
uv run eif upgrade matters/three-tier-app/aws dev- Python
>= 3.11with uv - Terraform
>= 1.5 - Cloud CLI configured with appropriate credentials or role
# install eif as a shell command
uv tool install git+https://github.com/giordanocardillo/eif
# or editable from a local clone (changes to eif.py take effect immediately)
uv tool install --editable .Then clone your component library and work from inside it:
git clone https://github.com/giordanocardillo/eif-library
cd eif-library
cp accounts.example.json accounts.json # fill in your credentialsAll eif commands are run from inside the library directory — eif finds the repo root by walking up to accounts.json.
# interactive — select provider, matter, and environment from menus
eif render
# non-interactive — pass provider, matter, environment directly
eif render aws three-tier-app dev# plan — render + terraform plan (no changes applied)
eif plan aws three-tier-app dev
# apply — render + terraform init + apply + snapshot on success
eif apply aws three-tier-app dev
# destroy — terraform destroy against the last rendered output
eif destroy aws three-tier-app dev
# rollback — pick a previous snapshot and re-apply
eif rollback aws three-tier-app devAll commands support interactive mode (no args) and non-interactive mode (<provider> <matter> <env>).
# set up state bucket / container / DynamoDB table via cloud CLI
eif init backend aws three-tier-app dev
# add a new account entry to accounts.json (one-time, per account)
eif add accounteif upgrade
eif upgrade aws three-tier-app devThe eif new commands interactively scaffold atoms, molecules, and matters. They detect available providers from providers/, check for existing versions, and create the correct directory structure with starter files.
# scaffold a new atom (prompts: name, provider, category)
eif new atom
eif new atom my-service # name pre-filled
# scaffold a new molecule (prompts: name, provider)
eif new molecule
eif new molecule my-service
# scaffold a new matter (prompts: name, provider)
eif new matter
eif new matter my-appIf the atom or molecule already exists the command reports the latest version and asks whether to create the next one (e.g. v2). Each scaffold emits starter main.tf, variables.tf, and outputs.tf (atoms/molecules) or composition.json, dev.example.json, prod.example.json, and main.tf.j2 (matters).
Matter is the only deployment entry point. Atoms and molecules are internal — use them as building blocks when authoring new matter templates, never deploy them directly.
EIF wraps the full Terraform deployment lifecycle. Every successful apply saves a snapshot of the rendered main.tf, enabling rollback to any previous configuration without touching the Terraform state file.
Add a backend key to any account in accounts.json to enable remote state. EIF will inject the correct backend {} block into the rendered output automatically.
| Provider | Backend type | Required fields |
|---|---|---|
| AWS | s3 |
bucket, region, dynamodb_table (locking) |
| Azure | azurerm |
resource_group_name, storage_account_name, container_name |
| GCP | gcs |
bucket |
// accounts.json — prod entry with S3 backend
{
"prod": {
"provider": "aws",
"aws_region": "us-east-1",
"assume_role_arn": "arn:aws:iam::...:role/EIFDeployRole",
"backend": {
"bucket": "my-tfstate-bucket",
"region": "us-east-1",
"dynamodb_table": "my-tfstate-locks"
}
}
}If no backend is configured, Terraform uses local state and EIF stores snapshots in .history/ (gitignored). This is suitable for solo developers; teams should configure a remote backend.
- Local (always):
.history/<env>/<timestamp>/main.tf— gitignored - Remote (if backend configured): uploaded alongside state in the same bucket/container
Rollback restores a previous rendered main.tf and re-applies it. Terraform computes the diff against the current live state and converges the infrastructure accordingly.
- Jinja2 → HCL renderer (
eif render) - Multi-environment and multi-account support
- Multi-cloud provider abstraction (pluggable
providers/<cloud>/) - Versioned atoms and molecules (
v1/,eif upgrade) - Scaffolding CLI (
eif new atom,eif new molecule,eif new matter) - Deployment lifecycle (
eif plan,eif apply,eif destroy,eif rollback) - Remote state management (S3 / Azure Blob / GCS backends)
- Snapshot history and rollback
- Backend bootstrap (
eif init backend) - Vulnerability scanning (
eif scanvia Trivy — auto-runs in plan/apply) - CI/CD pipeline examples (GitHub Actions / Azure DevOps)
- Cost estimation integration
- OPA/policy-as-code hook before apply
- Create
providers/<cloud>/provider.tf.j2with theterraform {}+provider {}block - Add atoms under
atoms/<cloud>/following the existing category structure - Add molecules under
molecules/<cloud>/composing those atoms - Add account entries to
accounts.jsonwith"provider": "<cloud>"
No changes to eif.py required.
Atoms, molecules, and matter contributions belong in eif-library. Please follow the existing file structure and naming conventions. Open an issue before submitting large structural changes.
Apache 2.0 © Giordano Cardillo