Internal Architecture
Main loop, heartbeat mechanism, graceful shutdown, and the grape-core shared package.
Internal Architecture
Tendril is a single Go binary with a straightforward architecture: a main poll loop, a heartbeat goroutine, and shared libraries from the grape-core package.
Main Loop
tendril start
├── Register with Trellis API
├── Start heartbeat goroutine (every 30s)
└── Poll loop (every 10s)
├── POST /api/jobs/claim
├── If job claimed:
│ ├── Assume cloud credentials
│ ├── Execute job (Terraform, ArgoCD, etc.)
│ ├── Stream logs
│ └── Report result
└── If no job: sleep 10s, repeatThe poll loop runs on the main goroutine. The heartbeat runs as a background goroutine. Both share the Tendril configuration.
Configuration
Tendril receives its configuration via environment variables:
| Variable | Description |
|---|---|
TRELLIS_URL | Base URL of the Trellis instance |
GRAPE_WORKER_ID | Unique Tendril identifier (assigned at registration) |
GRAPE_WORKER_TOKEN | Authentication token for API calls |
GRAPE_WORKER_MODE | cloud-hosted or self-hosted |
SUPABASE_S3_ENDPOINT | S3 endpoint for Terraform state |
SUPABASE_S3_REGION | S3 region |
SUPABASE_STORAGE_KEY_ID | S3 access key |
SUPABASE_STORAGE_SECRET_KEY | S3 secret key |
INFRACOST_API_KEY | Optional, for cost estimation |
Heartbeat Mechanism
The heartbeat goroutine sends POST /api/workers/heartbeat every 30 seconds with:
- Worker ID
- Current status (ONLINE or DRAINING)
- Currently executing job ID (if any)
Trellis uses the heartbeat to:
- Mark Tendrils as ONLINE (heartbeat within 60s) or OFFLINE (heartbeat missed)
- Detect dead Tendrils and requeue their jobs
- Track which Tendril is running which job
If a Tendril crashes mid-job:
- Heartbeat stops arriving
- After 60 seconds, Trellis marks the Tendril OFFLINE
- If no log updates for 5+ minutes, the job is marked FAILED
- User can retry the job — Terraform state is preserved in S3
Graceful Shutdown
When Tendril receives SIGINT or SIGTERM:
- Sets status to DRAINING — stops accepting new jobs
- Sends a DRAINING heartbeat to Trellis
- If a job is in progress, waits for it to complete (up to 10-minute grace period)
- Sends final heartbeat with OFFLINE status
- Exits
ECS Fargate sends SIGTERM when stopping a task, so Tendrils always attempt a graceful shutdown.
API Client
The WorkerAPIClient handles all communication with Trellis:
| Method | Endpoint | Purpose |
|---|---|---|
ClaimJob() | POST /api/jobs/claim | Atomic job claiming |
SendHeartbeat() | POST /api/workers/heartbeat | Keepalive |
UpdateJobStatus() | PUT /api/jobs/{id}/status | Status transitions |
SendLogs() | POST /api/jobs/{id}/logs | Log batch delivery |
UploadPlanArtifact() | POST /api/jobs/{id}/plan-artifact | Plan JSON upload |
DownloadPlanArtifact() | GET /api/jobs/{id}/plan-artifact | Plan JSON download |
All requests include X-Worker-ID and X-Worker-Token headers for authentication.
Shared grape-core Package
Tendril and Grape CLI both import from packages/grape-core/:
| Package | Purpose |
|---|---|
provisioner/ | Bootstrap, deploy, destroy orchestration |
terraform/ | Terraform CLI wrapper (init, plan, apply, destroy) |
cloud/aws/ | AWS SDK operations (STS, resource discovery) |
cloud/gcp/ | GCP SDK operations (WIF, resource discovery) |
cloud/azure/ | Azure SDK operations (federated auth, resource discovery) |
argocd/ | ArgoCD installation and configuration |
helm/ | Helm chart operations |
git/ | Git clone, branch, commit, push |
k8s/ | kubectl operations |
infracost/ | Infracost CLI wrapper and output parsing |
This shared package ensures both the CLI and Tendril use the same provisioning logic.