Skip to content

giordanocardillo/eif

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

46 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

EIF Logo

  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

License: Apache 2.0 Terraform Provider Agnostic Python Status Library

Build infrastructure the way nature builds matter — atom by atom.

Philosophy · Model · Providers · Structure · Renderer · Environments · Versioning · State · Usage · Roadmap


◈ Philosophy

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.


⬡ The Model

EIF organizes Terraform code into three hierarchical levels of abstraction:

◉ Atom — Level 01

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/.


◈ Molecule — Level 02

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 cloudfronts3.domain, waf.arn
db AWS rds + sg sg port derived from engine → rdssg.id
lambda-svc AWS lambda + sg lambdasg.id
single-page-application Azure blob + frontdoor frontdoorblob.primary_web_endpoint
single-page-application GCP gcs + cdn + armor cdngcs.bucket_name, armor.id

◆ Matter — Level 03

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, no environment key — 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

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" }
}

◫ Structure

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

◐ Renderer

The eif CLI takes a matter directory and an environment name. It:

  1. Loads accounts.json from the repo root
  2. Loads composition.json from the matter directory
  3. Loads <env>.json from the matter directory
  4. Renders providers/<cloud>/provider.tf.j2provider_block
  5. Builds a src dict mapping each molecule name to its resolved source path
  6. Merges account config, flat env vars, and environment into the template context
  7. 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               ──┘

◎ Environments

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" }
}
  • dev and test authenticate via named AWS CLI profiles
  • prod deploys to a separate AWS account via role assumption
  • Azure and GCP accounts follow the same pattern with their own auth fields

◑ Versioning

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

▶ Usage

Prerequisites

  • Python >= 3.11 with uv
  • Terraform >= 1.5
  • Cloud CLI configured with appropriate credentials or role

Install

# 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 credentials

All eif commands are run from inside the library directory — eif finds the repo root by walking up to accounts.json.

Render only

# interactive — select provider, matter, and environment from menus
eif render

# non-interactive — pass provider, matter, environment directly
eif render aws three-tier-app dev

Full deployment lifecycle

# 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 dev

All commands support interactive mode (no args) and non-interactive mode (<provider> <matter> <env>).

Bootstrap remote state

# 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 account

Upgrade molecule versions

eif upgrade
eif upgrade aws three-tier-app dev

Scaffold new components

The 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-app

If 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.


⊙ State

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.

Remote backends

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.

Snapshot storage

  • Local (always): .history/<env>/<timestamp>/main.tf — gitignored
  • Remote (if backend configured): uploaded alongside state in the same bucket/container

Rollback

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.


◌ Roadmap

  • 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 scan via Trivy — auto-runs in plan/apply)
  • CI/CD pipeline examples (GitHub Actions / Azure DevOps)
  • Cost estimation integration
  • OPA/policy-as-code hook before apply

⬡ Contributing

Adding a new cloud provider

  1. Create providers/<cloud>/provider.tf.j2 with the terraform {} + provider {} block
  2. Add atoms under atoms/<cloud>/ following the existing category structure
  3. Add molecules under molecules/<cloud>/ composing those atoms
  4. Add account entries to accounts.json with "provider": "<cloud>"

No changes to eif.py required.

General contributions

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.


◈ License

Apache 2.0 © Giordano Cardillo


EIF · Elemental Infrastructure Framework · Provider Agnostic · Terraform

About

Elemental Infrastructure Framework

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages