Open Source

Two Tools, One Job: Why I Started Building Conn3x

#Open Source#Conn3x#Go#OpenTofu#Infrastructure as Code#Ansible
James Conway10 min read

TL;DR: I've started building Conn3x, an open-source tool that creates your servers and configures them from one binary, with one plan and one state. It's built on OpenTofu, so your Terraform providers come with it. It's early, v0.5.0 is out, and the long-term goal is to follow OpenTofu into the Cloud Native Computing Foundation.

Visit conn3x.org

Creating a server and setting it up are one job. Ask anyone who’s done it for a living and they’ll tell you it takes two tools.

Terraform or OpenTofu builds the machine. Ansible configures it. And in between sits a seam that nobody really owns: a local-exec here, an inventory script there, a sleep 60 because the box wasn’t ready for SSH yet. It works. Until the day it doesn’t, and you’re reading two sets of logs to work out which half let you down.

I’ve started building something to close that seam. It’s called Conn3x.

Two Tools, Two States, One Server

The seam is the part nobody owns

I’ve spent 28 years in IT, and I look after a handful of servers on my own today, with a few hundred on the way. That number changes how you think about glue code. A workaround you tolerate on five boxes becomes a chore on fifty and a liability on five hundred.

The handoff between the two tools is where it hurts. I wrote about it in more detail on the Conn3x blog, but the short version goes like this:

  • Boot races. The cloud says the server exists. SSH says it doesn’t, yet. So you add retries and sleeps and hope.
  • Glue. Something has to carry the new server’s address from one tool to the other. Usually that’s a script you wrote and now maintain.
  • Two previews, two states. Terraform shows you its plan. Ansible’s --check --diff is a separate run, with separate state, or none at all.
  • Leftovers. Scale a pool down and the inventory can still list a server you destroyed last week.

None of that is anyone’s fault. Each tool does its own half well. What’s missing is the half in the middle.

What Conn3x Does

One binary, one plan, one state

Conn3x adds a circuit block to the HCL you already write. A circuit says which hosts to configure, how to reach them over SSH, and the steps to run on them. It sits in the same configuration as the servers, so a circuit’s hosts can point straight at a resource that doesn’t exist yet.

Here’s a small piece of one, taken from the quick start:

circuit "web" {
  hosts = { web1 = "203.0.113.10" }

  step "nginx" {
    package {
      name = "nginx"
    }
  }
  step "running" {
    service {
      name    = "nginx"
      state   = "started"
      enabled = true
    }
  }
}

Run conn3x plan and you get one preview. The droplet about to be created sits next to the exact package, file and service changes headed for it. conn3x apply does both. Nothing gets installed on your servers along the way: no agent, no Python. Conn3x connects over plain SSH, checks the host key every single time, and runs ordinary shell commands as root or through sudo.

One run, both layers: the droplet and the nginx steps in the same plan, applied in one go, then conn3x drift catching a hand edit and exiting 2. Re-typed from what Conn3x really prints, with the plan body trimmed and made-up timings.

Two more commands matter to me, because they’re the ones I’d reach for on a Monday morning.

conn3x drift tells you what somebody changed by hand, across the cloud and the servers both. It exits with code 2 when it finds something, so a scheduled job can shout about it. And conn3x convert ansible turns an existing playbook into circuits. Whatever it can’t translate, it marks with TODO(convert) and leaves for you to finish, rather than guessing.

Infographic: how Conn3x provisions and configures in one plan. Today the job takes two tools with a hand-made seam between them: boot races, glue code, two previews and stale inventory. A circuit is HCL beside the resource, with hosts, ssh, step and handler blocks. One conn3x plan lists the droplet and the account and key headed for it, twelve changes in one state. Apply runs over plain SSH with nothing installed, the host key always checked and a three-minute wait for boot. conn3x drift reads both layers and exits 2 if anything moved. conn3x convert turns an Ansible playbook into circuits and marks what it can’t translate TODO(convert). Six Linux families today, tested on four with every change; network devices and Windows are planned.

How Conn3x does provisioning and configuration in one plan. Every value is from the v0.5.0 README, the examples and the command help.
Read the infographic as text
  • Today the job takes two tools: OpenTofu or Terraform creates the machine, Ansible sets it up, and the seam between them is yours to maintain: local-exec, inventory scripts, sleep-and-retry loops, Python on the controller and every host. It shows up as boot races, glue code, two previews with two states, and inventory that still lists a server you removed.
  • A circuit is HCL in the same file as your resources. hosts says which machines, as a literal or an expression over a resource that doesn’t exist yet. ssh holds the user, key and become. Each step uses one module (package, template, service, user, cron, sysctl, firewall and more), reads the host, shows a diff, then applies. A handler runs only when a step that changed something notifies it. Every step becomes an ordinary resource underneath, so plan, state and drift already work.
  • One conn3x plan lists the cloud and the hosts together. The DigitalOcean example plans twelve changes for three Rocky Linux 10 droplets: three host keys, three droplets, three deploy accounts and three authorized keys, all in one state. The address is d.ipv4_address, known after apply, never written to an inventory file. Remove a host from the set and the next plan destroys that droplet and its steps, deleting its deploy key first.
  • Apply runs over plain SSH as root or through sudo. Nothing is installed on the server: no agent, no Python. The host key is pinned with host_key or read from known_hosts, and there is no way to turn the check off. The ssh timeout defaults to three minutes so a booting machine is retried, not raced. Host keys generated in the same configuration and installed by cloud-init mean even the first connection to a new droplet is verified.
  • conn3x drift reads every cloud resource and every host step again and reports what no longer matches the last apply. It changes nothing. Exit 0 means no drift, 2 means something moved (so a scheduled job can alert), 1 is an error. Edit /etc/motd by hand and drift names the file; conn3x plan shows the line-by-line diff and conn3x apply puts it back.
  • conn3x convert ansible -i inventory site.yml turns a playbook into circuits. Tasks it can show mean the same thing become steps. Anything else stays in the file as a comment marked TODO(convert), with the task, file and line it came from and the reason.
  • Conn3x configures Rocky, RHEL, AlmaLinux, Fedora, Debian and Ubuntu today, and end-to-end tests run on every change against Rocky Linux 10, Ubuntu 24.04, Debian 13 and Fedora 45. Jump hosts and ~/.ssh/config arrived in v0.2.2, write-only secrets in v0.3.0, for_each on steps in v0.4.0, locale and atomic writes in v0.5.0. Network devices are planned next, then Windows. Neither ships today.

Built on OpenTofu, on Purpose

Your providers come with you

I didn’t want to write yet another provisioning engine. The world has a very good one already, and it’s open source.

So Conn3x starts from OpenTofu and tracks its releases. Every Terraform and OpenTofu provider works unchanged, and so do your modules and your state. If you’ve got years of HCL behind you, none of it is wasted. The files that came from OpenTofu keep their original copyright and licence headers, and I’m grateful to the OpenTofu and Terraform communities for the work that makes that possible.

That decision also explains the licence. Conn3x is MPL-2.0, the same as OpenTofu. Remember that, because it matters in a minute.

Where It Is Today

Early, and I'd rather say so

The repository is only days old, and v0.5.0 is already out. That’s quick, and I won’t pretend otherwise: this is a young project.

Here’s what’s real right now. Conn3x configures Rocky, RHEL, AlmaLinux, Fedora, Debian and Ubuntu. Every change runs end to end against Rocky Linux 10, Ubuntu 24.04, Debian 13 and Fedora 45 before it merges. It covers the everyday work: files and templates, packages, services, users and SSH keys, cron and timers, firewalls, SELinux and more. Secrets can reach a server without ever landing in the plan or the state. Releases are signed and come with SBOMs.

Network devices come next, then Windows. Neither is there yet.

The Long Game: The CNCF

Going where OpenTofu went

What I’d love, eventually, is for Conn3x to become a Cloud Native Computing Foundation project.

Why hand it over? Because infrastructure tooling should outlive the person who started it. Nobody wants to bet their servers on one man’s laptop. A neutral home means the project, its name and its domains don’t depend on me, and that’s exactly the promise you want from a tool that touches every box you run.

It isn’t a short road, and there are two things in the way.

The first is people. The project’s governance says the move happens once Conn3x has maintainers from more than one organisation. One person in one company isn’t a foundation project, and it shouldn’t pretend to be.

The second is that licence. The CNCF normally expects Apache-2.0. OpenTofu is MPL-2.0 too, and in March 2025 the CNCF’s Governing Board approved an exception that let it keep its licence. A month later, on 23 April 2025, OpenTofu was accepted into the CNCF Sandbox. That’s the route I want Conn3x to take, the same way, for the same reason.

To be clear about where things stand: nothing has been applied for yet. This is the goal, not the status.

Kick the Tyres

And tell me what's missing

On a Mac it’s one line:

brew install conn3xorg/tap/conn3x

There are Linux and macOS downloads for amd64 and arm64 on GitHub. If you’d rather not point it at a real server yet, the repo has a try-it-locally example that runs against a throwaway container in about five minutes. The docs walk through the rest in about fifteen.

And if you’d like to help it get to the CNCF, the best thing you can do right now is use it and tell me what’s wrong. Ideas and feedback go in Discussions. Code is welcome too. Every commit needs a DCO sign-off, and there’s no CLA. There never will be.

Releases go out on conn3x.org and on X at @conn3xone. Come and break it.

// Let's talk

You have ideas.
I have IT solutions.

Fifteen minutes on Zoom is usually enough to tell whether I'm the right person for what you're building.

Denver, CO · Available for architecture, infrastructure, and DevOps engagements