Skip to content

Repository files navigation

Odin

License: Apache 2.0 CI Python

Design AWS architectures on a canvas, compile them to Terraform, and run the result on your laptop with IAM enforced.

Odin is a local cloud you draw. Drag S3, RDS, Lambda, EC2 and the rest onto a canvas and wire them together. Odin compiles that drawing into OpenTofu configuration and applies it against its own AWS-compatible gateway, which provisions each resource as a running service: Postgres for RDS, RustFS for S3, a Lima VM for EC2, a RIE container for Lambda. The permission edges you draw become policy the gateway checks on every API call, so an unauthorised request comes back AccessDenied.

The translation is deterministic. The same canvas always produces byte-identical HCL, Terraform reads back into canvas nodes the same way every time, and no model call sits anywhere in that path.

Contents

Install · What you can do · Apply · What an edge means · Diagrams · Terminal · AI · How it works · Known limits · Security · Contributing · Where it's going

Odin — a VPC/Subnet/EC2 stack, an SG, S3/SQS/SNS/DynamoDB/RDS, a Lambda, an ECS service, an IAM role and an ECR repo, drawn on the canvas with an IAM permission edge (EC2 → S3, GetObject/PutObject/ListBucket)

Draw it, press Apply, watch it come up. Three resources placed on the canvas, applied, and healthy, with a RustFS bucket, a dynalite table and a Postgres container behind those badges. The clip runs at roughly 4×; the apply it records took 103 seconds.

Three resources drawn on odin's canvas, applied, and going healthy — S3, DynamoDB and RDS badges turning green, with the database's live host:port shown on the node

IAM permissions are edges you draw, and the canvas updates live when anything changes it, including the CLI. This clip authors the edge with odin canvas set, and the already-open browser converges it with no reload.

An IAM permission edge appearing on odin's canvas, from an EC2 instance to an S3 bucket, labelled GetObject, PutObject, ListBucket

The { } button shows the Terraform the canvas compiles to — the same HCL that Apply runs.

odin's code panel scrolling through the Terraform generated from the canvas: aws_dynamodb_table, aws_db_instance and aws_s3_bucket resources

Install

macOS with Homebrew. This installs colima, the docker CLI, opentofu, uv and lima, starts colima, installs odin, and runs odin doctor:

curl -fsSL https://raw.githubusercontent.com/kessler-frost/odin/main/install.sh | sh
odin start          # serves http://localhost:4200 and backgrounds itself

Draw something from the sidebar and press Apply. The Events tab streams the tofu apply output, badges go healthy, and the { } button shows the generated Terraform.

To remove everything the installer put on the machine, including the Homebrew packages it added:

curl -fsSL https://raw.githubusercontent.com/kessler-frost/odin/main/uninstall.sh | sh
Manual install
brew install colima docker opentofu uv lima
colima start
git clone https://github.com/kessler-frost/odin && cd odin
uv sync --extra dev
uv run odin doctor        # every prerequisite, with a fix line for each

odin doctor reports on colima, docker, opentofu, lima and the claude CLI, and exits non-zero when something needed is missing, so it works as a CI preflight too.

What you can do

Drag any of these onto the canvas, wire them up, and press Apply:

Kind What it becomes
S3 A RustFS bucket, addressable with boto3
RDS A Postgres container, with a live DATABASE_URL on the node
SQS / SNS goaws queues and topics, subscriptions included
DynamoDB A dynalite table
ElastiCache A Redis container
Lambda A container running the AWS Lambda Runtime Interface Emulator
ECS Containers scheduled per task, with placement and rolling updates
EC2 A Lima VM, joined to the environment's Nebula mesh
VPC / Subnet / SG Nebula networks and compiled firewall rules
IAM role, ECR, Secrets, SSM, CloudWatch Logs, ALB Gateway-backed AWS APIs
Event triggers A schedule, a queue, or a bucket write really invokes a Lambda

Geometry carries meaning. A node drawn inside a VPC or Subnet box belongs to it, which compiles to vpc_id and subnet_id, and an ECS box drawn inside an EC2 box runs its tasks in that VM. One node can expand to several Terraform resources: an ALB becomes a load balancer plus a target group plus a listener, and a Lambda drawn without a role gets one generated for it.

Environments are independent. --env staging and --env prod keep separate canvases, containers and state. odin envs lists them, and odin env rm <name> decommissions one — teardown, then the environment itself.

Apply

Apply commits the canvas as the desired state, generates Terraform, runs tofu apply against the gateway, and waits for what it built to answer before reporting success. A resource that never becomes healthy fails the apply, and the output names which one and why.

odin apply --env prod
odin world --env prod          # one line per resource, with its live facts
odin destroy --env prod        # tear the resources down, keep the environment
odin env rm prod               # ...and remove the environment itself

odin destroy and odin env rm are different verbs. Destroy keeps the environment on purpose: its desired state is what makes a retry possible and its reconciler is what converges the next apply. env rm is the decommission — the same teardown, then the .odin/prod/ directory, the gateway credentials it issued, its synthesized control-plane records, its reconciler, and its entry in odin envs. It is not undoable (odin export --env prod first if you might want it back), and it exits non-zero, having deleted nothing, if the environment is not actually gone.

If a container is killed or removed out of band, the next apply rebuilds it and tells you what that cost:

note: desired state applied; rds app-db was re-created because container
odin-rds-prod-app-db is not running (exit 137) (its data survived — the
container is new, the volume holding the database is not)

Each RDS instance keeps its data on a named Docker volume, so a repair replaces the container and not the database. Odin checks that volume before it says so, and says the opposite when it is gone.

What an edge means

An edge from a workload to a resource is usually a permission. Draw one, pick the actions, and the gateway compiles them into policy it checks on every request. A call with no matching grant gets AccessDenied.

The edge type comes from what you connected: lambda → dynamodb is an IAM grant with sensible defaults, sg → ec2 is group membership, ec2 → subnet is containment.

Eight of the lines you can draw mean two things at once: rds or elasticache against ecs or lambda, in either direction. The workload needs the producer's address, and it may also need a grant on it — in AWS both readings are true at the same time. Odin does not choose between them for you: the picker lists both and you tick the ones you mean, so one line can carry both. Ticking the connection on rds → ecs writes DATABASE_URL into the service's environment, resolved to the real endpoint when the container starts.

Two more lines decide something besides access. A Log Group drawn to a workload becomes the group its output really lands in, rather than a second empty group beside the one the runtime writes to. An ECR repository drawn to an ECS service becomes that service's image, resolved to the registry address odin published. In both cases the permission alone used to be the whole of it, which meant the line looked like it wired the thing it granted access to and did not.

A drawn permission is emitted as a real aws_iam_role_policy on the workload's role, naming a real ARN — arn:aws:s3:::uploads and arn:aws:s3:::uploads/* for a bucket grant — so the file means the same thing on Amazon as it does here. The gateway matches either form: it reports odin's node label for a request and reduces the ARN back to it, which is what keeps a permission enforced locally and portable at the same time.

Driving it from a terminal

The UI is one client and the CLI is another, both going through the same HTTP API.

odin canvas set architecture.json --env prod   # author a canvas from a file
odin translate --env prod                      # print the Terraform it compiles to
odin import-tf ./terraform-project             # read an existing project back in
odin chat "give the worker read access to uploads" --env prod
odin export prod backup.tar.gz                 # and `odin import` to restore

docs/cli.md has the full surface, the JSON shapes, and the exit codes and output contracts a CI job can depend on.

AI: two features, one switch

Two features call a model, and the translation is never one of them.

odin chat turns plain English into canvas edits and applies them directly. The canvas is where you review them: the change appears in your open tab, and Cmd-Z undoes it. It never presses Apply, so building from the drawing stays yours.

$ odin chat "give the thumbnailer lambda read access to the uploads bucket"
Granted thumbnailer read access to the uploads bucket.
  - draw a iam edge from 'thumbnailer' to 'uploads' granting s3:GetObject, s3:ListBucket
canvas saved (4d18a5091659) — Cmd-Z in the UI undoes it

It works in named operations, so every change is one reviewable line and anything you did not ask for is impossible to reach. It declines to write what odin derives from geometry or from the live world, and it treats renaming as its own explicit act, because a label is the resource's name and a rename destroys and recreates. It sees field names and never their values, which keeps an RDS password out of the prompt.

"What's wrong here?" selects nodes in the UI and asks why they are broken. Odin gathers each node's config, observed phase, crash verdict, recent events and a tail of its logs, then answers in plain English with per-node suspects. It reads state and returns prose, and it can change nothing.

Both are off until you turn them on. The top bar carries an AI OFF switch, and odin makes no model call of any kind while it reads that way. Flip it and both features come alive; flip it back and they say so and carry on, with the debug panel answering with the reason and odin chat reporting agent unavailable while changing nothing.

Setting ODIN_AI in the environment overrides the switch in both directions, which is what a CI job or a headless run wants. The switch renders disabled and names the variable when that happens, so it never looks like a control that does nothing.

How it works

A canvas is the desired state. spec/ stores it as append-only, content-addressed revisions, agent/hcl.py compiles it to HCL, simulate/ runs tofu apply against gateway/, and reconcile/ observes what exists and projects it back as node status.

How an apply works — diagram
+-----------------------------------+
|                                   |
|               Canvas              |    <----+
|                                   |         |
+-----------------------------------+         |
                  |                           |
               compile                        |
                  |                           |
                  |                           |
                  v                           |
+-----------------------------------+         |
|                                   |         |
|             Terraform             |         |
|                                   |         |
+-----------------------------------+         |
                  |                           |
             tofu apply                       |
                  |                           |
                  |                           |
                  v                           |
+-----------------------------------+         |
|                                   |         |
|            odin gateway           |         |
|                                   |         |
+-----------------------------------+         |
                  |                           |
              provision                       |
                  |                           |
                  |                           |
                  v                           |
+-----------------------------------+         |
|                                   |         |
| Postgres, RustFS, goaws, Lima VMs |   observe
|                                   |
+-----------------------------------+

Terraform never talks to Amazon. tofu apply is pointed at odin's gateway, which speaks enough of S3, RDS, SQS, SNS, DynamoDB, Lambda, ECS, EC2, IAM, ECR, Secrets Manager, SSM, CloudWatch Logs and ELBv2 for the AWS provider to apply against it. Status flows the other way on its own loop, so a container that dies turns its node red without anyone pressing anything.

How a permission is enforced — diagram
+-----------------------------------+
|                                   |
|   boto3 call from your workload   |
|                                   |
+-----------------------------------+
                  |
                  |
                  v
+-----------------------------------+
|                                   |
|       verify SigV4 signature      |
|                                   |
+-----------------------------------+
                  |
                  |
                  v
+-----------------------------------+
|                                   |
| classify: s3:GetObject on uploads |
|                                   |
+-----------------------------------+
                  |
                  |
                  v
<----------------------------------->
|                                   |
|   does the APPLIED IAM allow it?  |---------------+
|                                   |               |
<----------------------------------->              no
                  |                                 |
                 yes                                |
                  |                                 |
                  v                                 v
+-----------------------------------+     +------------------+
|                                   |     |                  |
|   run it against the substitute   |     | 403 AccessDenied |
|                                   |     |                  |
+-----------------------------------+     +------------------+

The gateway holds a per-environment key for each workload, so it knows which node is calling. The edges you drew compile to policy statements, and a call is matched against them by action and resource before it reaches any substitute.

docs/architecture.md is the picture version — a diagram per service showing what really runs underneath, and one for the whole system. GitHub renders those inline; docs/architecture.html is the same thing styled, self-contained, and offline — open it in a browser. docs/internals.md has the architecture in full, and how each claim in this README is verified.

Known limits

The ones most likely to matter:

  • An emitted IAM policy names resources by label, not ARN. It round-trips through odin perfectly; taken to Amazon each policy needs its Resource rewritten as an ARN.
  • An RDS instance has no snapshots and no backups. Its data survives a container replacement (a named volume), and odin destroy deletes it along with the volume — there is nothing to restore from afterwards. odin volumes shows which volumes odin is holding and which belong to no live environment; odin env rm <env> reclaims that env's.
  • Import is narrower in --live mode, and a live-imported RDS arrives with odin's default password, because no AWS API returns one.
  • Lambda dependencies are vendored only. A function can be a whole directory (sourceDir), so it can import its own modules and carry whatever you have installed into that directory — but odin never runs a package manager and fetches nothing at apply time. One version, no S3-deployed packages.
  • Nebula is single-host. The mesh, firewall and per-VM daemons work; a second machine joining the same environment is still to come.

docs/limits.md has the complete list, including every argument odin re-emits with its own value.

Security

Odin has no authentication of its own. The control app binds 127.0.0.1 by default, and applying a canvas runs what is on it: container images, EC2 user-data as root, Lambda code. That is what the tool is for, and it means a canvas from someone else deserves the same caution as a shell script you are about to run.

Canvas secrets — an RDS password, a secret or ssm value — are stored and used in cleartext across the canvas, Stack revisions, world.json, events.jsonl, and the generated Terraform and state. Those files are 0600 and .odin/ is 0700. SECURITY.md has the full model.

Contributing

Issues and pull requests are welcome. uv run pytest runs the suite, and uv run pytest -m integration runs the slow tests that need Colima and OpenTofu. CONTRIBUTING.md has the rest.

Where it's going

NORTHSTAR.md has the direction and ROADMAP.md has what comes next, including closing the known limits above.

Acknowledgements

Odin stands on OpenTofu, the Terraform AWS provider, RustFS, goaws, dynalite, Nebula, Lima, Colima, FastAPI and React Flow.

License

Apache License 2.0 — see LICENSE.

About

An intelligent canvas for orchestrating infrastructure and application deployments.

Topics

Resources

Contributing

Security policy

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages