017 Project 17: SSH Remote Orchestrator
017 Build an SSH Remote Orchestrator
Run commands on many hosts concurrently over SSH and aggregate results.
host list -> worker pool -> ssh exec per host -> collect stdout/stderr -> summary
Setup
go mod init example.com/gossh-orchestrator
go get golang.org/x/crypto/ssh@latestFull main.go
package main
import (
"flag"
"fmt"
"os"
"strings"
"sync"
"time"
"golang.org/x/crypto/ssh"
)
type result struct {
host string
out string
err error
}
func run(host, user, keyPath, cmd string, timeout time.Duration) result {
key, err := os.ReadFile(keyPath)
if err != nil {
return result{host: host, err: err}
}
signer, err := ssh.ParsePrivateKey(key)
if err != nil {
return result{host: host, err: err}
}
cfg := &ssh.ClientConfig{
User: user,
Auth: []ssh.AuthMethod{ssh.PublicKeys(signer)},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
Timeout: timeout,
}
c, err := ssh.Dial("tcp", host+":22", cfg)
if err != nil {
return result{host: host, err: err}
}
defer c.Close()
s, err := c.NewSession()
if err != nil {
return result{host: host, err: err}
}
defer s.Close()
out, err := s.CombinedOutput(cmd)
return result{host: host, out: string(out), err: err}
}
func main() {
hostsArg := flag.String("hosts", "", "comma-separated host list")
user := flag.String("user", "root", "ssh user")
key := flag.String("key", os.Getenv("HOME")+"/.ssh/id_rsa", "private key path")
cmd := flag.String("cmd", "uname -a", "command to run")
workers := flag.Int("w", 10, "worker count")
timeout := flag.Duration("t", 5*time.Second, "ssh timeout")
flag.Parse()
if *hostsArg == "" {
fmt.Fprintln(os.Stderr, "provide -hosts")
os.Exit(2)
}
hosts := strings.Split(*hostsArg, ",")
jobs := make(chan string)
results := make(chan result)
var wg sync.WaitGroup
for i := 0; i < *workers; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for h := range jobs {
results <- run(strings.TrimSpace(h), *user, *key, *cmd, *timeout)
}
}()
}
go func() {
for _, h := range hosts {
jobs <- h
}
close(jobs)
wg.Wait()
close(results)
}()
ok := 0
for r := range results {
if r.err != nil {
fmt.Printf("[%s] ERROR: %v\n", r.host, r.err)
continue
}
ok++
fmt.Printf("[%s]\n%s\n", r.host, strings.TrimSpace(r.out))
}
fmt.Printf("done: %d/%d succeeded\n", ok, len(hosts))
}Run
go run . -hosts "10.0.0.11,10.0.0.12" -user root -cmd "uptime"Security Notes
- Replace
ssh.InsecureIgnoreHostKeywith known-hosts validation in production. - Use dedicated restricted SSH keys for automation.
Step-by-Step Explanation
- Model jobs, workers, and outputs explicitly.
- Bound concurrency using worker pools and buffered channels.
- Use
sync.WaitGroupfor lifecycle control. - Aggregate worker results in one place.
- Verify behavior under both normal and failure paths.
Code Anatomy
- Producer pushes jobs into a channel.
- Workers consume jobs and emit results.
- Aggregator merges results and prints summary.
Learning Goals
- Build leak-free goroutine patterns.
- Balance throughput and resource limits.
- Understand fan-out/fan-in architecture.