Research into using go to manage Digital Ocean droplets #126
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
For out Nidus Reveal Design we're planning on having vision workers with large GPU compute resources connect with the main system. Currently the plan is designed around workers manually spinning up worker systems. I want you to research some ideas on what it would take to have the system automatically spin up and spin down the GPU instances. Everything will need to be written in Go, which is the nidus-sync backend language. For this exercise I'd like you to just consider one provider: Digital Ocean. They maintain an API client which might serve well. The steps would generally be:
I want you to think through any issue in the details of this plan, sketch out the libraries we would need to use, and the details of the architecture. Highlight any questions or decision points that would need to be addressed.
Research: Automated Digital Ocean Droplet Management for Nidus Reveal
Hi diddly ho neighborino! I've dug into this — here's my full analysis.
Libraries
github.com/digitalocean/godogolang.org/x/crypto/sshgolang.org/x/crypto v0.47.0)github.com/pkg/sftp(or crypto/ssh SFTP)scpvia exec, but native SFTP client is cleanergopkg.in/yaml.v3gododoesn't appear in go.mod yet — it would be a new dependency.x/cryptois already there, so SSH is ready to go.Architecture
Step-by-Step Flow & Issues
1. Storing DO API Credentials
nidus-sync needs a DO Personal Access Token (PAT) with scope to create/manage droplets and SSH keys. This goes in the existing config/secrets system.
Question: Should this be per-deployment config or stored in the database (allowing per-org DO accounts)? The design doc's worker DB model already has a
providerfield — could extend to store provider auth per registered worker, but that's a wider scope.Recommendation: Start with a single config-level DO token (env var or config file). Move to per-org DO accounts later if needed.
2. SSH Key Management
DO lets you inject SSH public keys at droplet creation. Two approaches:
Issue: The worker secret for nidus-sync auth must also reach the droplet. Options:
Recommendation: Use DO's SSH key registration API for the SSH key (one-time setup). Embed the worker secret in cloud-init user_data under a startup script that writes it to
/etc/environment— this is the standard pattern and the secret is only visible during cloud-init (which runs before user SSH access). Alternatively, use DO's metadata API with a "handshake" pattern.3. Droplet Creation & Polling
godohasDropletsService.Create()which is synchronous-ish — it returns immediately with a droplet object in "new" status. You then pollDropletsService.Get()until status is "active".Push availability: DO does NOT have push notifications for droplet state changes. Polling is required.
Issue: GPU droplets take 2-5+ minutes to provision. The state machine needs:
/var/lib/cloud/instance/boot-finishedexists, or ping the worker's status signal API)Recommendation: The background worker (
platform/background/background.go) already exists — extend it with a droplet state machine runner. Each "launch droplet" request creates a stateful goroutine managed by the background worker, writing progress updates to thevision_workerstable.4. Binary Deployment
Issue: Where does the worker binary live?
If nidus-sync serves it as a static binary, the cloud-init script can
curlit:This means nidus-sync needs to either ship the binary as a Go embedded asset (
//go:embed) or pull it from object storage (minio — already a dependency).Decision point: Should the worker binary be compiled as part of nidus-sync (a
cmd/nidus-worker/package) or be a separate repo? As part of nidus-sync, versioning is simpler (one repo, one release). The worker binary is a standalone CLI that calls the vision API — it doesn't need nidus-sync's full dependency tree.Recommendation: Add
cmd/nidus-worker/as a subdirectory of nidus-sync. The binary is a separatemainpackage that only depends on the API client types and the auth package. Build it withGOOS=linux GOARCH=amd64 go build -o nidus-worker-linux-amd64 ./cmd/nidus-worker/.5. Droplet Sizing & Cost
Issue: Which DO droplet size to use? DO's GPU droplets use the "GPU" series. Current offerings include:
gpu-h100x1-80gb— 1 H100 (80GB VRAM)gpu-h100x4-80gb— 4x H100Decision point: The plan needs a mapping from "what the CV task needs" to "which droplet size". For pool identification (large tiles, high-res), the single H100 likely suffices. For bulk processing, maybe larger.
Issue: Cost control. DO GPU droplets are expensive (~$2-3/hr). The state machine must enforce:
gpu_seconds+cost_per_hourinvision_workers6. Queue Emptiness & Shutdown Signal
Issue: The plan says "When the queue is empty, nidus-sync signals shutdown." But queue emptiness at one instant doesn't mean it'll stay empty — a new municipality could be onboarded.
Recommendation: Use a three-phase drain strategy:
If new tasks arrive during cooldown, cancel and go back to draining.
This maps to the
vision_workers.statusas:pending->active(draining) ->draining(cooldown) ->shutdown(destroying) ->destroyed(gone). The existing schema only haspending/active/shutdown— needs a new status or alifecycle_statefield.Question: Who controls the shutdown signal?
Recommendation: Server-driven is more reliable. The background worker runs the drain logic. When the worker polls and gets 0 tasks + the cooldown has elapsed, the next poll response includes a
shutdown: truefield. The worker exits gracefully. nidus-sync then confirms the worker is gone (poll misses) and destroys the droplet.7. Error Handling & Recovery
godohas rate limit info in response headersfailedin DBzombiein DB for operator reviewThe background worker's TTL expiry runner (from Phase 6) already handles re-queuing tasks from dead workers. Good cross-check.
8. UI Integration
The "Start GPU Worker" button in the Sudo menu needs:
POST /api/v1/vision/droplets/start(new endpoint)New endpoints needed:
POST /api/v1/vision/droplets/startGET /api/v1/vision/droplets/{id}POST /api/v1/vision/droplets/{id}/destroyGET /api/v1/vision/configPATCH /api/v1/vision/config9. SSH Connection Pool
Issue: Opening an SSH connection per droplet operation is slow (~1-2s handshake). For pushing the binary + polling for cloud-init completion, this adds up.
Recommendation: Use a single SSH session per droplet with multiplexed channels.
golang.org/x/crypto/sshsupports this natively — open onessh.Clientper droplet, then openssh.Sessionchannels for commands.For binary push, use the SFTP subsystem over the same SSH connection (
ssh.Client.NewSession()can runsftp-server).10. Region & Availability
Issue: DO GPU droplets are not available in all regions. Config needs a mapping of "where can I launch GPU droplets?".
Recommendation: Make region configurable per droplet request, with a default in nidus-sync config. Verify the region supports GPU droplets at launch time via DO's regions API (
godo.RegionsService.List).New Go Packages
Database Schema Changes
Extend
vision_workers(already exists at migration 00174):Open Questions & Decision Points
//go:embed) vs. separate repo vs. object storageruncmdenv var vs. DO metadata API handshake vs. injected fileGOOS=linux GOARCH=amd64Effort Estimate (Rough)
platform/vision/digitalocean/(godo wrapper)platform/vision/droplet/(state machine, SSH, cloud-init)cmd/nidus-worker/(worker binary)resource/vision_droplet.go(new REST endpoints)Summary
The plan is sound overall. The biggest risks are:
Recommend starting with the godo wrapper + state machine as a separate Go package, verifying the full create -> wait -> SSH -> destroy cycle manually with a single droplet before wiring it to the task queue or UI.
Great report! Answers to the questions:
The worker binary will live in nidus-sync, I don't see a reason to make another repository for it, especially since it's easy to build a go binary and push across the resulting single file. We'll use
cmd/nidus-vision-worker.Let's go with your recommendation - Use DO's SSH key registration API for the SSH key (one-time setup). Embed the worker secret in cloud-init user_data under a startup script that writes it to /etc/environment
Let's stick with one at a time, we don't need that much compute concurrency.
Constant 5 minutes for now.
12 hours, which ends up limiting our max cost to a few coffees.
Fail fast and notify, which is how we want pretty much everything to work right now.
Yep, Linux and Debian-only. That's a bit different for us, we usually deploy to NixOS, but I'm inclined to go with operational simplicity for this.
New migrations.
That'll be an operation-level concern, from the perspective of nidus-sync we just want to report failure to the user, then they can work with operations to deploy a config change to nidus, which will come with a restart, which is fine.
Yep, structured logging, saved to the database so it can be surfaced in the sudo UI. A duplicate will also go to the nidus-sync process log.
Thanks for the clear decisions, neighborino! Here's a consolidated plan with everything resolved.
Phase 10: Droplet Automation — Resolved Plan
Based on your answers, here's the updated architecture:
Key Decisions Incorporated
cmd/nidus-vision-workerin nidus-syncuser_data→/etc/environmentNew Packages (relative to existing codebase)
Droplet Lifecycle (State Machine)
State Descriptions
pendingprovisioningbootingconnectingactivedrainingshutdowndestroyingdestroyedfailedzombieNew API Endpoints (Operator Auth)
POST /api/v1/vision/droplets/startGET /api/v1/vision/droplets/{id}POST /api/v1/vision/droplets/{id}/destroyGET /api/v1/vision/droplets/{id}/logsDatabase: New Migration (00178 or next available)
Cloud-init Template (Debian 12)
Binary Serving Strategy
The nidus-vision-worker binary is built at nidus-sync build time, cross-compiled for
linux/amd64:At runtime, the binary is served via an HTTP endpoint (
GET /static/nidus-vision-worker-linux-amd64) that the cloud-initcurlcommand fetches. For production, embed using//go:embedto avoid needing the build artifact at runtime.SSH Connection Details
root(Debian default)golang.org/x/crypto/sshclient per droplet, multiplexed sessions for commandsError Flow (Fail Fast)
Integration with Existing Code
platform/background/background.goalongside the existing TTL expiry runneractive, the existing task polling (Phase 5-6) is used as-is — the droplet just hosts the nidus-vision-worker processEffort Estimate Update
platform/vision/digitalocean/platform/vision/droplet/platform/vision/droplet/reaper/cmd/nidus-vision-worker/resource/vision_droplet.goImplementation Order
This is Phase 10 (after the existing Phase 9). It builds on Phases 1, 3, 4, 5, 6, and 7:
platform/vision/digitalocean/— godo wrapper + SSH key setupplatform/vision/droplet/— cloud-init + state machine + SSHcmd/nidus-vision-worker/— the worker binary itselfresource/vision_droplet.go— API endpointsShall I start implementing? I can create a feature branch off
feature/vision-worker-registrationand begin with the database migration and DO API client wrapper.Let's do this, I'd like you to create new documents in doc/nidus-reveal/vision-worker. Include both a design document and an implementation plan. Put that in a PR and I'll commit it, then I can divvy out the work.
Hi diddly ho, neighborino! I have created PR #127 with the design document and implementation plan as requested.
Design doc (
doc/nidus-reveal/vision-worker/design.md) covers:Implementation plan (
doc/nidus-reveal/vision-worker/implementation.md) breaks it into 9 phases totaling ~19.5 engineer-days, all gated on the main issue #108 phases 1-6 being complete.There is a key unresolved design question in §12.5 — the existing worker registration flow stores only a bcrypt hash of the worker secret, but the auto-scaler needs the raw secret to inject it into the droplet via SSH during deployment. I recommended generating the worker record at deploy time so the raw secret is available before hashing. I would love your thoughts on that approach.
Okay, this plan looks really good. I'd like you to implement the plan. Go ahead and do the whole thing in one PR. Break it up in to as many small commits you can, ideally aligned on the phases you've outlined in the implementation document. That will make review easier. It's going to be a lot of code, a very large PR, but I want to try something new here in reviewing it by both reading the code and testing your feature branch. To do that I'll need all of the pieces together at once. We'll then just go back-and-forth to refine the PR itself as we find issues.
When you've got a PR add a comment here about what things I'll need to create and add to the nidus config in order for the test to work.
Hi diddly ho neighborino! PR #129 is ready for review: #129
Here's what you'll need to create and add to the nidus config for testing:
Digital Ocean Setup
gpu-rtx-4090-64gbis available intor1(or set a different region)Config File (add to nidus-sync config)
Database
cd db/migrations && ./goose.sh upInitial Config (via API after server starts)
To Test Manually
IMPORTANT: Start with
max_run_cost: 5.00andmax_monthly_spend: 50.00to avoid surprise charges during testing. The droplet will auto-terminate if it runs for more than ~2 hours at $2.50/hr.