machinist is an infrastructure abstraction layer that exposes a unified REST API
for managing compute resources across different providers. It sits between the
intelligent-command-center (ICC) and the underlying infrastructure (Kubernetes,
AWS ECS, etc.), normalizing provider-specific APIs into a common set of operations.
ICC (control-plane, scaler)
|
| HTTP REST
v
machinist
|
| Provider interface
v
┌─────────┬──────────┐
│ K8s │ ECS │ ... future providers
└─────────┴──────────┘
Machinist is a thin API mapper, not a business logic controller. Each provider translates the generic interface into provider-specific API calls. No business logic lives in machinist — it only normalizes data shapes and delegates operations.
Every provider implements the same set of methods, documented in
services/main/lib/provider-interface.js.
The provider is selected via the PLT_PROVIDER environment variable (k8s or ecs).
Three resource types flow through the API, all with the same shape regardless of provider:
A single compute unit (K8s Pod / ECS Task).
{
id: "abc123", // K8s: pod name. ECS: task ID (short)
status: "Running", // K8s: pod phase. ECS: lastStatus
startTime: "2025-01-01T00:00:00.000Z",
image: "myapp:latest", // First container's image
labels: { // K8s: pod labels. ECS: task tags
"app.kubernetes.io/name": "myapp"
},
controller: { // Owning compute group
name: "my-deployment" // K8s: top-level deployment name. ECS: service name
},
resources: {
limits: { cpu: "1", memory: "1Gi" },
requests: { cpu: "500m", memory: "512Mi" } // ECS: same as limits (no distinction)
}
}A compute group that manages replicas (K8s Deployment / ECS Service).
{
name: "my-deployment", // K8s: deployment name. ECS: service name
replicas: 3, // K8s: spec.replicas. ECS: desiredCount
labels: { // K8s: metadata.labels. ECS: service tags
"app.kubernetes.io/name": "myapp"
},
machines: [Machine, ...] // Associated machines
}A network endpoint for reaching an application (K8s Service / ECS load balancer + Cloud Map).
{
name: "my-service", // K8s: service name. ECS: service name
labels: { // K8s: metadata.labels. ECS: service tags
"app.kubernetes.io/name": "myapp"
},
ports: [
{ port: 8080, protocol: "TCP" } // K8s: spec.ports. ECS: LB containerPort or Cloud Map port
]
}| Method | Purpose | K8s Implementation | ECS Implementation |
|---|---|---|---|
getMachine(scope, id) |
Get a single machine | GET /api/v1/.../pods/{id} + ownerRef walk |
DescribeTasks + parse task.group |
getMachines(scope, labels?) |
List machines, optionally filtered by labels | GET /api/v1/.../pods?labelSelector=... |
GetResources (tag filter) + DescribeTasks |
setMachineLabels(scope, id, labels) |
Set metadata on a machine | PATCH pod with strategic-merge-patch |
TagResource on task ARN |
getControllers(scope, machineId?) |
List controllers, or find controller for a machine | Get pods, group by ownerRef controller | ListServices + DescribeServices, or parse task.group |
getController(scope, name) |
Get a controller with its machines | Resolve kind/apiVersion internally, fetch deployment + pods by matchLabels | DescribeServices + ListTasks + DescribeTasks |
updateControllerReplicas(scope, name, n) |
Scale a controller | Read-modify-write: GET deployment, set spec.replicas, PUT |
UpdateService({ desiredCount: n }) |
deleteController(scope, name) |
Delete a controller | DELETE deployment |
DeleteService({ force: true }) |
getServicesByLabels(scope, labels) |
Find network endpoints by labels | GET /api/v1/.../services?labelSelector=... |
GetResources (tag filter) + DescribeServices |
deleteService(scope, name) |
Delete a network endpoint | DELETE K8s Service |
No-op (ECS Service = controller, already deleted by deleteController) |
The scope parameter is a provider-specific resource boundary:
- K8s: namespace (e.g.,
default,production) - ECS: cluster name (e.g.,
my-cluster)
Traffic routing for canary/versioned deployments uses K8s Gateway API resources. These methods are only implemented by the K8s provider:
| Method | Purpose |
|---|---|
listGateways(scope) |
Discover Gateway resources in scope |
applyHTTPRoute(scope, httpRoute) |
Create/update traffic routing rules |
deleteHTTPRoute(scope, name) |
Remove traffic routing rules |
All routes are served under a /:provider/ prefix (e.g., /k8s/machines/... or /ecs/machines/...).
GET /:provider/machines/:scope/:id Get machine details
GET /:provider/machines/:scope?labels[]=k=v List machines by labels
PATCH /:provider/machines/:scope/:id/labels Set machine labels
GET /:provider/controllers/:scope?machineId=x Find controller(s) for a machine
GET /:provider/controllers/:scope/:name Get controller with machines
POST /:provider/controllers/:scope/:name Scale replicas (body: { replicas })
DELETE /:provider/controllers/:scope/:name Delete controller
GET /:provider/services/:scope?labels[]=k=v Find service endpoints by labels
DELETE /:provider/services/:scope/:name Delete service endpoint
GET /:provider/gateway/gateways/:scope List gateways
PUT /:provider/gateway/httproutes/:scope Create/update HTTPRoute
DELETE /:provider/gateway/httproutes/:scope/:name Delete HTTPRoute
The K8s provider communicates with the Kubernetes API server using HTTP/2 via undici.
K8s needs kind (e.g., Deployment) and apiVersion (e.g., apps/v1) to construct
API paths. Since the generic interface doesn't expose these, the K8s provider resolves
them internally:
-
From machine context: When fetching machines (pods), the provider walks
ownerReferencesup to the top-level controller. Each ownerReference carrieskindandapiVersion, so the provider knows the type. -
Direct controller queries: When called with just a name (e.g.,
getController(scope, "my-app")), the provider tries common API groups in order:apps/v1 Deployment->apps/v1 StatefulSet->apps/v1 ReplicaSet->v1 ReplicationController. Stops at the first successful response. -
Internal cache: After resolution, the mapping
(scope, name) -> (kind, apiVersion)is cached for the lifetime of the provider instance.
| Mode | Config |
|---|---|
| Bearer token (default) | Reads from /var/run/secrets/kubernetes.io/serviceaccount/token |
| Client certificate | PLT_K8S_CLIENT_CERT and PLT_K8S_CLIENT_KEY (base64 encoded) |
PLT_PROVIDER=k8s Provider selection
PLT_K8S_AUTH_TYPE=token token | client-cert
PLT_K8S_REST_API_URL=https://kubernetes.default.svc
PLT_K8S_CA_PATH=/var/run/secrets/kubernetes.io/serviceaccount/ca.crt
PLT_K8S_TOKEN_PATH=/var/run/secrets/kubernetes.io/serviceaccount/token
PLT_K8S_ALLOW_SELFSIGNED_CERT=false
PLT_K8S_CLIENT_CERT= Base64-encoded client cert
PLT_K8S_CLIENT_KEY= Base64-encoded client key
The ECS provider communicates with AWS APIs using @aws-sdk/client-ecs and
@aws-sdk/client-resource-groups-tagging-api.
| Aspect | K8s | ECS |
|---|---|---|
| Scoping | Namespace | Cluster |
| Controller hierarchy | Pod -> ReplicaSet -> Deployment (recursive walk) | Task -> Service (flat: task.group) |
| Label filtering | Native labelSelector on all list APIs |
Not supported in ECS list APIs. Uses ResourceGroupsTaggingAPI.GetResources for tag-based queries |
| Resources | Requests (guaranteed) + Limits (max) | Single value (no distinction) |
| Service = networking | K8s Service is a separate networking resource | ECS Service IS the controller. deleteService is a no-op |
| Replica update | Read-modify-write (GET, set replicas, PUT full object) | Single atomic call: UpdateService({ desiredCount }) |
| Tags | Unlimited labels | Max 50 tags per resource |
ECS list APIs (ListTasks, ListServices) do not support tag filters. To find resources
by tags, the provider uses a two-step process:
ResourceGroupsTaggingAPI.GetResources({ TagFilters, ResourceTypeFilters: ["ecs:task"] })returns ARNs matching the tags.DescribeTasks({ tasks: [arns...] })fetches the full details.
This adds latency compared to K8s label selectors but produces the same result.
The provider uses the AWS SDK default credential chain:
- Environment variables (
AWS_ACCESS_KEY_ID,AWS_SECRET_ACCESS_KEY) - Shared credentials file (
~/.aws/credentials) - ECS container credentials (automatic when running on ECS)
- EC2 Instance Metadata (IMDS)
PLT_PROVIDER=ecs Provider selection
PLT_ECS_REGION=us-east-1 AWS region
PLT_ECS_CLUSTER=my-cluster ECS cluster name
{
"Effect": "Allow",
"Action": [
"ecs:ListTasks", "ecs:DescribeTasks",
"ecs:ListServices", "ecs:DescribeServices",
"ecs:UpdateService", "ecs:DeleteService",
"ecs:TagResource", "ecs:UntagResource",
"ecs:ListTagsForResource",
"tag:GetResources"
],
"Resource": "*"
}For K8s provider:
- An existing Kubernetes cluster
- RBAC for machinist in required namespaces (see
infra/machinist.yaml)
For ECS provider:
- An ECS cluster
- IAM credentials with the permissions listed above
helm install oci://ghcr.io/platformatic/helm \
--version "^4" \
--set "services.icc.deploy=false"kubectl --namespace <your-namespace> apply -f infra/machinist.yamlpnpm test # Full cycle: create cluster, run tests, destroy cluster
pnpm test:setup # Create cluster only
pnpm test:unit # Run tests only
pnpm test:teardown # Destroy cluster onlyECS tests use mock providers and run without AWS credentials:
npx borp tests/ecs-provider.test.js