extra-ssh-bash runs one command across a Terraform-managed cluster in parallel
A Go concurrent SSH executor that fans out bash commands to every host, fed by GitLab-backed Terraform state.
What it is
extra-ssh-bash solves an awkward but common chore: you have a cluster of servers — say three EC2 instances running Docker — and you want to run the same command on all of them at once. The repository, Apache-2.0 licensed with a go.mod pinned to Go 1.22.5, is described in its README as "primarily a bash utility," but it ships three pieces: a not-yet-implemented bash script (esb.sh), the working Go concurrent executor under cmd/, and a Terraform template that builds the cluster in the first place.
The practical, usable part today is the Go program. The README is candid about this: "the bash utility is not yet implemented." What works is the concurrent SSH command runner offered by the exec-multi-remote-ssh-bash-cmd package at cmd/main.go.
How it gets its targets
The tool has two ways to learn which hosts to hit, and they are mutually exclusive.
If you pass --tfdir, it treats that directory as a Terraform project whose state lives in GitLab's HTTP backend. It runs terraform output -json public_ips by default and parses the returned IP list; the code also accepts a public_ip output variable, which is read with plain terraform output (no JSON). That is why the flags include --api, --id for the GitLab project, and --token: the program reconstructs the state address as
{api}/projects/{id}/terraform/state/{tf-state-name}
and sets the TF_HTTP_* environment variables (TF_HTTP_ADDRESS, TF_HTTP_LOCK_ADDRESS, TF_HTTP_UNLOCK_ADDRESS, and credentials) so that Terraform commands run with the right backend. The state name is derived from the directory's basename plus a -tfstate suffix, falling back to default-tfstate.
Alternatively you can skip Terraform entirely by passing --ipcsv as a comma-separated list of addresses. When --ipcsv is non-empty it takes precedence; otherwise the tool assumes you want the Terraform path.
The concurrent runner
The core loop in main() is straightforward concurrency over SSH. For every target IP, it builds one command:
ssh -i {key} -o IdentitiesOnly=yes -o StrictHostKeyChecking=no -o CheckHostIP=no {user}@{ip} {bash}
Each invocation runs inside a goroutine guarded by wg.Add(1)/Done(), and the whole fleet is throttled through a semaphore from github.com/andreimerlescu/go-sema, sized to runtime.GOMAXPROCS(0). Results are collected into a map keyed by IP address; each entry holds the command, stdout, and stderr.
Output comes back two ways. Without --json it prints a per-host text block ("Host {ip}: ... Command: ..."). With --json, the whole results map is marshaled to JSON — which is what makes this scriptable from other tooling. A small validator (commonValidator) treats an execution as successful when stderr is empty and stdout is non-empty; failures are logged rather than fatal.
The README carries two practical warnings about the --bash payload: don't pass blocking commands like yes, tail, or watch, because the concurrent runtime will misbehave, and you cannot use a pipe (|) inside --bash.
The Terraform template
Under terraform/docker-cluster/ sits docker-cluster.tf, an AWS template that provisions a Docker-ready cluster. It declares an HTTP backend pointed at GitLab's state API (with lock/unlock addresses and POST/DELETE methods), the hashicorp/aws provider, and an aws_instance.docker_member resource with count = var.docker_instance_count, defaulting to 3 instances of type t3.nano. It also creates an aws_key_pair.
The template is full of placeholder tokens — REPLACE_WITH_SSH_PUBLIC_KEY, subnet-REPLACEME, SG1 through SG5, a hardcoded AMI for Ubuntu 24.04 LTS, and the region/availability zone defaults in us-east-1. The README's tutorial walks you through replacing each one with sed, generating an ed25519 keypair, running terraform init/plan/apply, then invoking the Go tool against the resulting public_ips output. A note warns that swapping the OS from Ubuntu (apt-get/ufw) to something like Rocky Linux (yum/firewalld) may break the template's user_data.
The bash side
For completeness, esb.sh is a skeleton: it sets up debug and noclobber flags, sources four library files (functions.sh, header.sh, validations.sh, actions.sh) from lib/, prints dice-glyph progress markers as each loads, then dispatches on an action parameter. Today the only case in that switch falls through to a fatal "unsupported action" error — consistent with the README's admission that the bash utility is not yet implemented.
Getting started
The intended workflow, per the tutorial:
- Clone the repo and copy
terraform/docker-cluster/docker-cluster.tfinto your own Terraform project directory. - Replace the placeholders (project ID, GitLab API URL, SSH public key, region, instance size, AMI, subnet, security groups).
- Configure AWS (
aws configure) and initialize/apply Terraform so state lands in GitLab's HTTP backend. - Run the Go tool with your flags:
go run . \
--api "https://gitlab.com/api/v4" \
--id 3 \
--token "$(cat "${HOME}/.secrets/docker-cluster-access-token")" \
--tfdir ~/work/terraform/docker-cluster \
--user ubuntu \
--key ~/.ssh/docker-cluster-master.pem \
--bash "docker ps" \
--json
The README shows the JSON response: one object per IP, each with cmd, stdout, and stderr. That is the whole value proposition — a single command fanned out across every member of a Terraform-managed cluster, returned in machine-readable form.
Who it's for
This targets operators who already keep their infrastructure state in GitLab (Community or Enterprise edition both work) and want a quick way to push identical commands at an entire fleet. It is small, single-purpose, and honest about its unfinished parts. If you need piped remote commands, blocking processes, or anything beyond running one command everywhere, the README tells you where it will bite.
andreimerlescu/extra-ssh-bash: 4 MB in 21 files, indexed Nov 9, 2026 after 0.00 hours of indexing. License: Apache-2.0. Source: https://github.com/andreimerlescu/extra-ssh-bash.
Ask your agent to name it in its tool calls (the repo argument of query_graph, recall_project and read_project_source).
Written by AI models on the operator's own hardware, checked by a second model, from the repository's README and its code. Back to the shelf