Skip to content

DevOps Without the Code: Infrastructure as Markdown

Once

I wanted to prove a point: you only need Markdown to build non-trivial infrastructure if your abstractions are powerful enough.

Take the once package, for example. It creates a complete platform on any cloud provider—handling DNS, SMTP, and TLS—to deploy your web applications. It’s a self-hosted alternative to Netlify, Vercel, or Fly.io that runs on your own VPS.

I started by manually drafting a simple plan in user-plan.md. I then handed that plan to Claude Code and asked it to refine the requirements. Claude generated final-plan.md, a detailed technical roadmap.

Finally, I asked the AI to execute the plan. Within three minutes, I had a working version of the once package tailored to my specific requirements.

The kicker? I didn’t write a single line of Clojure code.

flow

Even though the underlying engine is Clojure, the configuration is entirely declarative.

  • The bb.edn file feels more like a Makefile than a programming script.
  • The EDN format is essentially JSON with a few more superpowers.
  • The logic is hidden; what’s left is just a list of parameters to tailor the behavior.

I can easily imagine a non-technical user asking an AI to “fill in the missing values” and run the tasks until they work.

How is this possible? BigConfig is the React of Agentic DevOps. Just as React allows you to build complex frontends using high-level components, BigConfig encapsulates messy Terraform and Ansible code into clean, reusable “infrastructure components.” It’s like HCL (Terraform) modules on steroids because it supports any tool—Ansible, Terraform, Helm, and more—under one unified, data-driven interface.

You don’t have to take my word for it. Below are the exact inputs I used so you can reproduce the experiment yourself.

user-plan.md
You need to create a project to deploy a web application.
This project contains only three files: a `bb.edn` file, a `deps.edn` file, and a `.envrc` file
The deps.edn will contain the dependency https://github.com/amiorin/once
You can download https://github.com/amiorin/once to see how it works.
https://github.com/amiorin/once is built on top of https://github.com/amiorin/big-config
You can download https://github.com/amiorin/big-config to see how it works.
The bb.edn contains a dependency :local/root to the current directory containing the file deps.edn that contains a dependency to https://github.com/amiorin/once
The domain is bigconfig.online.
The host is www.bigconfig.online.
The container is ghcr.io/amiorin/big-config-website:latest
The compute provider is OCI with 6 GB, 1 core, and 50 GB of disk.
The dns provider is Cloudflare.
The smtp provider is Resend.
The backend provider is S3.
You need to need to create the `.envrc` environment variable for the credentials, I will provide the values later.

Claude expanded my draft into a robust plan:

final-plan.md
# Deployment plan: bigconfig.online
A three-file project that uses `amiorin/once` to provision OCI + Cloudflare DNS + Resend SMTP + an S3 tofu backend, and hands off container lifecycle + TLS to `basecamp/once` running on the VM.
`amiorin/once` is built on top of `big-config`. Before wiring anything, clone both repos locally and confirm the assumptions in the "Open questions" section below — several env-var names and behaviors here are placeholders until verified against the source.
- https://github.com/amiorin/once
- https://github.com/amiorin/big-config
- https://github.com/basecamp/once (installed on the VM; runs the container + handles TLS)
## Files
- **`deps.edn`** — depends on `io.github.amiorin/once` via `:git/sha`. No `:paths`, no override namespaces.
- **`bb.edn`**`:deps {:local/root "."}` to pull the sibling `deps.edn`, `:tasks` that `:require` once's tasks so `bb once create` / `bb once delete` are exposed, and a top-level Clojure data structure holding the project options (profile, host, container image, "don't create APEX", etc.) that once's tasks consume.
- **`.envrc`** — exports the variables listed below. Follow once's convention of sourcing `.envrc.private` for secrets, and add `.envrc.private` to `.gitignore`.
## Targets
- **Domain**: `bigconfig.online`
- **Host**: `www.bigconfig.online`**do not create an APEX record**; if once's `website` profile creates one by default, override it from the `bb.edn` options
- **Container**: `ghcr.io/amiorin/big-config-website:latest` — must publish a `linux/arm64` variant, since the OCI free tier is Ampere A1
- **Compute**: OCI `VM.Standard.A1.Flex`, 1 OCPU, 6 GB RAM, 50 GB boot volume, Ubuntu LTS
- **DNS**: Cloudflare
- **SMTP (domain provisioning + outbound mail)**: Resend
- **Tofu backend**: S3 — the state bucket must exist before the first `tofu init`; pre-create it manually or add a bootstrap task in `bb.edn`
## Container lifecycle
`amiorin/once` provisions the VM and (via Ansible) installs `basecamp/once` on it. `basecamp/once` then pulls and runs the container and handles TLS via Let's Encrypt. As a result, `amiorin/once` does **not** need a `docker run` role, a systemd unit, or a compose file of its own — but `basecamp/once` has its own configuration surface that must be wired through.
## Environment variables (`.envrc`)
Values to be supplied later. `BC_PAR_*` are once/big-config parameter overrides; exact spellings must be confirmed against `src/` in `amiorin/once`.
**Domain & tofu backend**
- `BC_PAR_DOMAIN=bigconfig.online`
- `BC_PAR_HOST=www.bigconfig.online`
- `BC_PAR_PROVIDER_BACKEND=s3`
- `BC_PAR_S3_BUCKET`
- `BC_PAR_S3_REGION`
- `AWS_ACCESS_KEY_ID`
- `AWS_SECRET_ACCESS_KEY`
**OCI (compute)** — once's OCI module will read some combination of these; confirm exact names:
- tenancy OCID
- user OCID
- API key fingerprint
- API private key path
- region
- compartment OCID
- availability domain
- SSH public key (authorized on the VM)
- SSH private key path (used by Ansible to log in)
**Cloudflare (DNS)**
- `BC_PAR_CLOUDFLARE_API_TOKEN`
- `BC_PAR_CLOUDFLARE_ZONE_ID`
**Resend**
- `BC_PAR_RESEND_API_KEY` — provisioning SPF/DKIM records on the domain
- `BC_PAR_RESEND_PASSWORD` — SMTP auth, passed into the container so the website can send mail
**basecamp/once (on the VM)**
- `BC_PAR_ONCE_LETSENCRYPT_EMAIL` — Let's Encrypt registration address
- `BC_PAR_ONCE_LICENSE_KEY` — only if `big-config-website` is deployed as a licensed Basecamp product; omit if it's a bring-your-own container
**Container registry (ghcr.io)** — only if the image is private:
- `GHCR_USERNAME`
- `GHCR_TOKEN`
## Guideline
Avoid writing Clojure source files. Everything is data: project options live in `bb.edn` as a Clojure map, secrets live in `.envrc` / `.envrc.private`. The only "code" is the `:tasks` wiring in `bb.edn` that hands the options map to once's tasks.
## Open questions to resolve before implementing
1. **Options loading.** Does `amiorin/once` accept project options as a data arg passed from `bb.edn`, or does it require a Clojure namespace on the classpath? Determines whether the "no source files" guideline is achievable as-is.
2. **Profile.** Does once's `website` profile already target `bigconfig.online` / `www` / the ghcr image? If not, how is it overridden from the `bb.edn` options map?
3. **Exact `BC_PAR_*` names** for OCI, SSH keys, container image, and basecamp/once inputs — derive from once's source rather than guessing.
4. **basecamp/once scope.** Does it host arbitrary containers, or does it require a product manifest? What does it actually need at install time (email, license, domain)?
5. **Image.** Is `ghcr.io/amiorin/big-config-website:latest` public, and does it publish `linux/arm64`?
6. **APEX.** Does the `website` profile create an apex record by default? If yes, wire the override through the `bb.edn` options map.
7. **S3 bucket bootstrap.** Pre-create manually, or automate via a bootstrap task in `bb.edn`?

This is the heart of the deployment: a concise mix of requirements, data structures, and tasks. Notice how it reads like a declarative configuration manifest rather than a complex program.

bb.edn
{:deps {bigconfig/online {:local/root "."}}
:tasks
{:requires ([big-config.render :as render]
[big-config.workflow :as workflow]
[io.github.amiorin.once.package :as package]
[io.github.amiorin.once.params :as params]
[io.github.amiorin.once.tools :as tools])
:init (def bigconfig-online
{:big-config.render/profile "online"
:big-config.workflow/params
{:package "online"
:domain "bigconfig.online"
:provider-compute "oci"
:oci-config-file-profile "DEFAULT"
:oci-compartment-id "FILL-ME-IN"
:oci-subnet-id "FILL-ME-IN"
:oci-availability-domain "FILL-ME-IN"
:oci-display-name "bigconfig-online"
:oci-shape "VM.Standard.A1.Flex"
:oci-ocpus 1
:oci-memory-in-gbs 6
:oci-boot-volume-size-in-gbs 50
:oci-boot-volume-vpus-per-gb 30
:oci-ssh-authorized-keys "~/.ssh/id_ed25519.pub"
:provider-smtp "resend"
:resend-server "smtp.resend.com"
:resend-port 587
:resend-username "resend"
:provider-dns "cloudflare"
:provider-backend "s3"
:s3-bucket "FILL-ME-IN"
:s3-region "eu-west-1"
:once {:applications [{:host "www.bigconfig.online"
:image "ghcr.io/amiorin/big-config-website"}]}}})
once {:doc "bb once create | bb once delete"
:task (package/once* *command-line-args* bigconfig-online)}
tofu {:doc "bb tofu render tofu:init tofu:apply:-auto-approve"
:task (tools/tofu* *command-line-args* (params/once-opts bigconfig-online))}
tofu-smtp {:doc "bb tofu-smtp render tofu:init tofu:apply:-auto-approve"
:task (tools/tofu-smtp* *command-line-args* (params/once-opts bigconfig-online))}
tofu-dns {:doc "bb tofu-dns render tofu:init tofu:apply:-auto-approve"
:task (tools/tofu-dns* *command-line-args* (params/once-opts bigconfig-online))}
tofu-smtp-post {:doc "bb tofu-smtp-post render tofu:init tofu:apply:-auto-approve"
:task (tools/tofu-smtp-post* *command-line-args* (params/once-opts bigconfig-online))}
ansible {:doc "bb ansible render -- ansible-playbook main.yml"
:task (tools/ansible* *command-line-args* (params/once-opts bigconfig-online))}
ansible-local {:doc "bb ansible-local render -- ansible-playbook main.yml"
:task (tools/ansible-local* *command-line-args* (params/once-opts bigconfig-online))}}}

By treating infrastructure as pure data, we remove the “syntax tax” of Clojure. Whether you are a seasoned developer or a non-technical founder, BigConfig provides a path to professional-grade DevOps without the steep learning curve.

Would you like to have a follow-up on this topic? What are your thoughts? I’d love to hear your experiences.