once is a BigConfig package for ONCE. This BigConfig package is an infrastructure automation tool that simplifies the provisioning and configuration of cloud resources using OpenTofu and Ansible. The audience is the vibe coder who wants to deploy his vibe coded application with a "one-click" experience.
It is built on top of big-config, leveraging its workflow and configuration management capabilities.
- End-to-End Orchestration: A seamless six-stage workflow:
- Infrastructure: Provisioning with OpenTofu.
- SMTP: Email infrastructure with OpenTofu (Resend).
- DNS: Domain configuration with OpenTofu (Cloudflare provider v5), including automatic SMTP records, apex (
@) and wildcard (*) A records proxied through Cloudflare, and a curated bundle of zone settings (TLS 1.3, strict SSL, always-use-HTTPS, etc.). - SMTP Post-Verification: Finalizing SMTP setup (e.g., domain verification) with OpenTofu.
- Local Config: Ansible on the local machine wires up
~/.ssh/configso the freshly provisioned host is reachable asHost oncefor the next stage. - Remote Config: Ansible on the remote host installs Docker and ONCE, provisions a restricted
deployuser for one-command redeploys, and reconciles the configured applications.
- OpenTofu Remote Backend: Support for remote state management using S3 or Cloudflare R2, automatically rendered for all Tofu-based stages.
- Multi-Cloud Support: Native templates for:
- DigitalOcean (
digitalocean) - Hetzner Cloud (
hcloud) - Oracle Cloud Infrastructure (
oci) - No-Infra (
no-infra): For when the server is already there.
- DigitalOcean (
- Dynamic Inventory: Automatically bridge the gap by generating Ansible inventory directly from OpenTofu outputs.
- SMTP Testing Ready: Automatically installs
s-nailand configures.mailrcon the remote host for immediate SMTP verification. - Restricted Deploy SSH: Provisions a
deployuser with NOPASSWD sudo limited toonce, and an SSHForceCommandBabashka script that accepts onlysudo once update <host>for hosts present inonce list(CI-friendly redeploys without root SSH). - Environment Overrides: Support for overriding any configuration parameter via environment variables (e.g.,
BC_PAR_DOMAIN). - Configurable Workflows: Execute complex multi-step processes like
tofu init/applyfollowed by multipleansible-playbookruns.
To use once, you need the following tools installed:
- Clojure: The core engine.
- Babashka: Recommended for running CLI tasks.
- OpenTofu: For infrastructure management.
- Ansible: For configuration management.
- AWS CLI: Required for S3 backend management.
- Cloud Credentials: e.g.,
DIGITALOCEAN_TOKEN,HCLOUD_TOKEN,CLOUDFLARE_API_TOKEN,RESEND_API_KEY, or OCI configuration.
You can override any parameter defined in options.clj using environment variables prefixed with BC_PAR_. The variable name is converted to lowercase, and underscores or dots are replaced with hyphens.
Example:
export BC_PAR_DO_TOKEN="your-digitalocean-token"
export BC_PAR_RESEND_PASSWORD="your-smtp-password"
export BC_PAR_DOMAIN="example.com"To enable the S3 backend for OpenTofu, set the following parameters:
export BC_PAR_PROVIDER_BACKEND="s3"
export BC_PAR_S3_BUCKET="your-tf-state-bucket"
export BC_PAR_S3_REGION="eu-west-1"To enable the Cloudflare R2 backend instead:
export BC_PAR_PROVIDER_BACKEND="r2"
export BC_PAR_R2_BUCKET="your-tf-state-bucket"
export BC_PAR_R2_ENDPOINT="https://<account-id>.r2.cloudflarestorage.com" # add `.eu` / `.fedramp` before `r2` for jurisdictioned buckets
export BC_PAR_R2_ACCESS_KEY_ID="your-r2-access-key"
export BC_PAR_R2_SECRET_ACCESS_KEY="your-r2-secret"These will be automatically merged into the workflow parameters.
The easiest way to interact with once is through the provided Babashka tasks.
Clone the repository and configure your options:
git clone https://github.com/amiorin/once
cd once
# Edit your chosen provider options
edit src/clj/io/github/amiorin/once/options.cljIn src/clj/io/github/amiorin/once/options.clj, you can switch the active profile used by Babashka by changing the bb definition:
;; options.clj
;; Switch between online, space, website, or no-infra
(def bb website)online, space, and website are application profiles — they pin a domain, package, and the list of containerized apps deployed by Ansible. online and space ride on oci; website rides on digitalocean. The space profile, for example, deploys a templated Pocketbase instance, while website deploys the bigconfig.ai sites.
All four profiles also merge in the deploy sub-profile, which carries two SSH public keys:
compute-pubkey— the operator's key (its private half must be loaded inssh-agentfor Ansible to reach the new VM on cloud providers;bb validatechecks this).deploy-pubkey— the key authorized on the remotedeployuser withForceCommand(CI-driven redeploys).
Override either per-environment via BC_PAR_COMPUTE_PUBKEY and BC_PAR_DEPLOY_PUBKEY.
Note: If you are using the no-infra profile, ensure your parameters are correctly prefixed (e.g., no-infra-compute-ip, no-infra-compute-user, no-infra-smtp-server).
Before provisioning, run a quick check that the active profile is well-formed, the required CLIs are installed, the credentials work, the referenced Docker images exist, and (for cloud compute profiles) :compute-pubkey is loaded in ssh-agent so Ansible can connect to the new host:
bb validateFor Cloudflare DNS profiles, validation also confirms the configured :domain is an active zone on the supplied Cloudflare account.
The once task handles the full lifecycle. You can pass multiple commands:
- Full Setup:
bb once create(Tofu -> Tofu SMTP -> Tofu DNS -> Tofu SMTP Post -> Ansible Local -> Ansible) - Tear Down:
bb once delete(Tofu SMTP Post Destroy -> Tofu DNS Destroy -> Tofu SMTP Destroy -> Tofu Destroy) - Sequential:
bb once delete create(Clean slate redeploy)
Compute resources are rendered with lifecycle { prevent_destroy = true } by default as a safeguard. To run bb once delete, first override it:
export BC_PAR_COMPUTE_PREVENT_DESTROY=falseOnce a stack is up, bb describe prints a human-readable status for the active profile: configured providers (compute, backend, SMTP, DNS), SSH reachability of the compute host, and every ONCE application discovered on the server with image, tag, running digest, registry digest, and whether an update is available. Most checks are soft failures; only a missing remote once command causes a non-zero exit.
bb describeYou can also run the underlying tools individually. Most tasks require a render step first to generate the necessary config files from templates into the .dist/ directory.
- OpenTofu (Infrastructure):
bb tofu render tofu:init tofu:apply:-auto-approve
- OpenTofu (SMTP):
bb tofu-smtp render tofu:init tofu:apply:-auto-approve
- OpenTofu (DNS):
bb tofu-dns render tofu:init tofu:apply:-auto-approve
- OpenTofu (SMTP Post-Verification):
bb tofu-smtp-post render tofu:init tofu:apply:-auto-approve
- Remote Ansible:
bb ansible render -- ansible-playbook main.yml
- Local Ansible:
bb ansible-local render -- ansible-playbook main.yml
You can trigger workflows directly from a Clojure REPL:
(require '[io.github.amiorin.once.package :as once])
(require '[io.github.amiorin.once.options :as options])
;; Run the "create" workflow using OCI profile
(once/once* "create" options/oci)- Template Rendering:
big-configtakes templates fromsrc/resourcesand your options to generate valid Tofu and Ansible files in.dist/. - Infrastructure Hook: When
createruns, it first executes OpenTofu to provision resources. - Inventory & Config Bridging: The Tofu output (like the new server IP or SMTP records) is captured using
tofu output --jsonand injected into the DNS configuration and Ansible inventory generation logic. - Local Finalization: The local Ansible playbook updates your local environment (e.g.,
~/.ssh/config) so the new server is reachable asHost oncebefore the remote stage runs. - Configuration: Ansible then connects to the new host using the dynamically generated inventory to apply your playbooks.
src/clj/.../once/:options.clj: Where you define your cloud profiles and credentials.package.clj: Defines the high-levelcreate/deleteworkflows.params.clj: Logic for extracting parameters from Tofu outputs.tools.clj: Implementation details for Tofu, Tofu SMTP, Tofu DNS, and Ansible wrappers.validation.clj: Profile schema, tool, credential, image, and ssh-agent checks (bb validate).describe.clj: Post-provisioning report (bb describe).
src/resources/.../once/tools/:tofu/: Multi-cloud.tftemplates.tofu-backend/: OpenTofu backend templates (S3, Cloudflare R2, local).tofu-smtp/: SMTP configuration templates (Resend).tofu-dns/: DNS configuration templates (Cloudflare).tofu-smtp-post/: SMTP post-verification templates (Resend).ansible/: Remote system playbooks.ansible-local/: Local machine configuration playbooks.
If you are contributing to once, you can use the following task to keep the code clean:
bb -tidyThis uses clojure-lsp to clean namespaces and format the source code.
Copyright © 2026 Alberto Miorin
Distributed under the MIT License.
