Runners
A runner is one deployable service (Cloud Run) with its own identity. Code
declares that it exists; fluffy-chainsaw.yaml decides what it may touch, how it is
reached, and how it builds.
Prerequisites
@org/fluffy-chainsawinstalled.- A make target per runner that builds its image and prints the image ref as the
last line of stdout (that's how
fluffy-chainsaw buildcaptures it). - The image follows the local dev-mode convention:
WORKDIRunder/repomirroring your repo layout (an app atapps/apiruns from/repo/apps/api) and an exec-formCMD ["node", "<entry>"].fluffy-chainsaw localmounts the checkout at/repoand wraps the CMD withnode --watch, so code edits restart the runner in-place; an image that doesn't conform is a hard error atlocaltime (deploy is unaffected).
The markers
cloudrun.entrypoint(name, handler?)
import { cloudrun } from "@org/fluffy-chainsaw";
cloudrun.entrypoint("orders-api", (req, res) => {
res.end("hello");
});
- Build time: declares that runner
orders-apimust exist. Existence only — everything else is tuned in yaml. - Runtime: with a
handler, serves HTTP onprocess.env.PORT(default 8080). With no handler it is a pure marker (use this when your real server lives elsewhere, e.g. an Express app started by the Docker CMD).
cloudrun.scheduled(path, handler) — cron routes
cloudrun.scheduled("/jobs/daily-digest", async (_req, res) => {
await sendDigests();
res.end("ok");
});
Pair each scheduled path with a cron expression in yaml:
runners:
orders-api:
trigger:
schedules:
- { path: /jobs/daily-digest, cron: "0 6 * * *" }
The pairing is two-way checked: a yaml schedule with no cloudrun.scheduled
handler (or vice versa) fails fluffy-chainsaw scan. Schedule and subscription paths on
one runner share a router, so they must be unique (E_TRIGGER_PATH_DUP).
fluffy-chainsaw.yaml
runners:
orders-api:
uses: # least privilege — a map per binding kind, NOT a flat list
databases: [users, orders]
buckets:
- { name: media, prefix: uploads/, writer: true }
publishes: [orders] # topics it may publish to
secrets: [stripe-key]
auth: true # opt into the auth plugin
kind: cloud-run # optional; only "cloud-run" today
trigger:
http: true # public; omit/false = private (IAM callers only)
ingress: all # all | internal | internal-load-balancing
schedules: [] # see above
subscriptions: [] # see pubsub.md
build:
target: orders-api # make target (default: runner name)
makefile: Makefile # optional
env: # string -> string, set on the service
LOG_LEVEL: info
vpc: # optional egress via a Serverless VPC connector
connector: my-connector
egress: private-ranges-only # all-traffic | private-ranges-only
Networking: two independent axes
- Auth — who may invoke it:
trigger.http. - Ingress — where it is reachable from:
trigger.ingress.
| Goal | yaml |
|---|---|
| Public (internet, unauthenticated) | trigger: { http: true } |
| Private (IAM-authenticated callers only) | omit trigger.http (or false) |
| Internal only (VPC / same project) | trigger: { ingress: internal } |
| Internal via load balancer | trigger: { ingress: internal-load-balancing } |
| Egress to private-IP resources | vpc: { connector: <name>, egress: private-ranges-only } |
The axes compose — e.g. a locked-down internal service:
trigger: { http: false, ingress: internal }
vpc: { connector: my-connector, egress: all-traffic }
Calling another runner (local)
import { runner } from "@org/fluffy-chainsaw";
const res = await fetch(`${runner.url("billing")}/charge`, { method: "POST" });
Under fluffy-chainsaw local, every runner gets RUNNER_URL_<NAME> pointing at its
sibling containers; runner.url(name) resolves it (and throws with a fix-it
message if the runner isn't declared or running).
Gotchas
- A yaml runner no
cloudrun.entrypointmatches →E_NOT_IN_CODE; a runner thatusesa database code never declared →E_USES_UNKNOWN. - The same code deployed as two runners holds two different privilege sets —
grants follow
uses, not the source. - A runner with push subscriptions must be private — see pubsub.md.