Lab I3: The Go SDK and firectl
Background
You have driven Firecracker by hand with curl --unix-socket, and in
Lab I1 you saw that the firecracker-containerd shim
does not type curl — it calls a Go library that builds those same REST requests.
That library is firecracker-go-sdk,
and the single-binary CLI built on it is
firectl. Together they are how almost
everyone who isn't AWS actually launches Firecracker: a typed Go API and a thin CLI over
the REST API and the jailer.
This lab is build-it. You will write a small Go program that launches and configures a
microVM through the SDK, run firectl as a one-shot CLI, and — most importantly — map both
back to the raw curl calls you already know, so you understand precisely what an SDK
does and does not add. The SDK is "an abstraction of the OpenAPI-generated client that
allows for convenient manipulation of Firecracker VM from Go programs": it is generated
from the same firecracker.yaml swagger spec that defines the REST API, wrapped in
ergonomic types. Knowing that the SDK is a generated client over the spec tells you
exactly where SDK bugs can and cannot be: a bug in request shaping is the SDK's; a bug in
how Firecracker answers a correctly-shaped request is Firecracker's. That boundary is the
whole point.
Note: The SDK's Go module path is
github.com/firecracker-microvm/firecracker-go-sdkand it tracks recent Go (1.23+, verify againstgo.mod). Its core types areMachine,Config,Drive, andNetworkInterface; you create a machine withNewMachineand start it withmachine.Start. Confirm every name below against the checkout — generated code and exported APIs drift across releases.
Why This Lab Matters for Contributors
- The SDK is the reference consumer of the REST API. When you propose an API change in Firecracker, "does this break the Go SDK's generated client?" is a question maintainers will ask — and you should be the one who already checked.
- Reproducing a bug "through the SDK" vs. "through
curl" is the cleanest possible bisection between an SDK problem and a Firecracker problem (Lab I4, Lab I5). - firectl is the fastest way to launch a microVM for ad-hoc debugging — far quicker than
four
curls — and you should have it in your toolbox. - It makes the API server and action channel deep dive concrete from the caller's side.
Prerequisites
| Requirement | Why |
|---|---|
Lab 1.3 — the raw curl boot | The thing the SDK abstracts |
| API endpoint map | The REST surface the SDK wraps |
Go 1.23+ (verify), a built firecracker binary, a kernel + rootfs | The toolchain |
# Confirm Go and grab the SDK; read the REAL required version and core types.
go version
git clone https://github.com/firecracker-microvm/firecracker-go-sdk
cd firecracker-go-sdk
grep -m1 "^go " go.mod
# These are the load-bearing types — confirm they still exist and their fields.
rg -n "type Machine struct|func NewMachine|func .*Machine. Start|type Config struct|type Drive |type NetworkInterface" . | head -30
# The SDK is generated from the swagger spec — find the generated client.
rg -n "go-swagger|swagger|client/operations|firecracker.yaml" . | head
How the SDK maps to the REST API
The mental model you must hold: the SDK is three layers, and only the middle one is hand-written.
your Go program
│ Machine / Config (ergonomic, hand-written, the "SDK")
▼
SDK convenience layer (Machine.Start, Machine.SetMetadata, jailer integration)
│ calls →
▼
OpenAPI-generated client (generated from firecracker.yaml — one func per endpoint)
│ HTTP/JSON over the Unix socket →
▼
firecracker REST API (PUT /boot-source, /drives, /machine-config, /actions, …)
│
▼
the VMM (action channel → VMM thread) ← everything below here is the SAME as curl
Every SDK call lands on an endpoint you already know. The mapping is mechanical:
| SDK construct | REST endpoint it ultimately calls | The curl you'd type by hand |
|---|---|---|
Config{ KernelImagePath, KernelArgs } | PUT /boot-source | --data '{"kernel_image_path":...,"boot_args":...}' |
Drive{ DriveID, PathOnHost, IsRootDevice } | PUT /drives/{id} | --data '{"drive_id":...,"path_on_host":...}' |
Config{ MachineCfg: VcpuCount, MemSizeMib } | PUT /machine-config | --data '{"vcpu_count":...,"mem_size_mib":...}' |
NetworkInterface{ ... HostDevName, MacAddress } | PUT /network-interfaces/{id} | --data '{"iface_id":...,"host_dev_name":...}' |
machine.Start(ctx) | PUT /actions {InstanceStart} | --data '{"action_type":"InstanceStart"}' |
machine.SetMetadata(ctx, ...) | PUT /mmds | --data '{...}' .../mmds |
Config{ JailerCfg: ... } | runs the jailer (Lab I2) | the whole jailer invocation |
The SDK adds convenience and lifecycle, not new capability: it knows the correct
ordering of pre-boot calls, it can spawn and supervise the firecracker process, it can
launch through the jailer, and it gives you Go types instead of stringly-typed JSON. It
adds nothing the REST API can't do — by construction, since it's generated from the
spec. Hold onto that: it is why "reproduce it with curl" is the decisive SDK-vs-FC test.
Step-by-Step Tasks
Step 1: A minimal SDK launcher
Write main.go that boots a microVM via the SDK. This mirrors your four curls exactly —
notice each field's REST twin.
// main.go — launch a microVM with firecracker-go-sdk.
// go mod init fcdemo && go get github.com/firecracker-microvm/firecracker-go-sdk
// Field/type names are version-sensitive — confirm against the SDK checkout.
package main
import (
"context"
"log"
fc "github.com/firecracker-microvm/firecracker-go-sdk"
models "github.com/firecracker-microvm/firecracker-go-sdk/client/models"
)
func main() {
ctx := context.Background()
socket := "/tmp/fc-sdk.sock"
cfg := fc.Config{
SocketPath: socket,
KernelImagePath: "./vmlinux", // → PUT /boot-source
KernelArgs: "console=ttyS0 reboot=k panic=1", // → boot_args
Drives: []models.Drive{{
DriveID: fc.String("rootfs"), // → PUT /drives/rootfs
PathOnHost: fc.String("./rootfs.ext4"),
IsRootDevice: fc.Bool(true),
IsReadOnly: fc.Bool(false),
}},
MachineCfg: models.MachineConfiguration{ // → PUT /machine-config
VcpuCount: fc.Int64(2),
MemSizeMib: fc.Int64(1024),
},
}
// NewMachine wires the generated client to the socket; the command launches firecracker.
cmd := fc.VMCommandBuilder{}.WithSocketPath(socket).
WithBin("./firecracker").Build(ctx)
m, err := fc.NewMachine(ctx, cfg, fc.WithProcessRunner(cmd))
if err != nil {
log.Fatalf("NewMachine: %v", err)
}
// Start = the pre-boot PUTs in the right order, then PUT /actions {InstanceStart}.
if err := m.Start(ctx); err != nil {
log.Fatalf("Start: %v", err)
}
log.Printf("microVM running; socket=%s", socket)
if err := m.Wait(ctx); err != nil {
log.Printf("Wait: %v", err)
}
}
go mod init fcdemo
go get github.com/firecracker-microvm/firecracker-go-sdk
go run .
Tip: If the API surface above doesn't compile, the SDK moved a field or a constructor option. That is expected and is itself the lesson — run the
rgfrom Prerequisites and read the SDK's ownexample_test.go/examples/for the current canonical usage:rg -n "NewMachine|VMCommandBuilder|WithProcessRunner" examples/ *_test.go.
Step 2: Prove the SDK is just the REST API underneath
This is the load-bearing exercise. Run your program, and while it's booting, drive the
same socket with curl to confirm the SDK and you are talking to one API:
# In one shell: go run . (leaves the socket up)
# In another: query instance state over the SDK's own socket.
curl -s --unix-socket /tmp/fc-sdk.sock http://localhost/ | python3 -m json.tool
# → the same instance-info JSON you'd get from any FC microVM. The SDK created a plain
# firecracker process; nothing proprietary sits between you and it.
Then do the reverse: capture what the SDK sent. Point the SDK at a socket while you watch the wire, or read the generated client to see the literal request bodies:
# The generated client encodes each endpoint — read the body it builds for boot-source.
rg -n "boot-source|PutGuestBootSource|BootSource" client/ | head
Conclusion to internalize: there is no magic. The SDK assembled the identical JSON you would have, in the identical order, and PUT it to the identical socket.
Step 3: Launch the same microVM with firectl
firectl collapses the whole sequence into one command. Install and run it:
git clone https://github.com/firecracker-microvm/firectl && cd firectl
make build || go build . # produces the firectl binary (verify target)
./firectl --help | head -40 # READ the real flags — they map 1:1 to REST fields
The canonical launch — note how the flags are just the REST config in CLI form:
sudo ./firectl \
--kernel=./vmlinux \
--root-drive=./rootfs.ext4 \
--ncpus=2 \
--memory=1024 \
--kernel-opts="console=ttyS0 reboot=k panic=1"
# Optional, mapping to the same endpoints you know:
# --tap-device=tap0/06:00:AC:10:00:02 → PUT /network-interfaces/{id}
# --vsock-device=/tmp/v.sock:3 → PUT /vsock
# --add-drive=./data.ext4:rw → PUT /drives/{id}
# --cpu-template=T2 → /machine-config cpu_template
# --socket-path=/tmp/fc.sock → the API socket
firectl is built on the Go SDK, which is built on the REST API. Three layers, one underlying machine. Confirm the dependency yourself:
grep firecracker-go-sdk go.mod # firectl imports the SDK
Step 4: Map every firectl flag and SDK field to an endpoint
Produce the mapping table for your version (the canonical deliverable). For each flag and
each Config field, name the REST endpoint and the JSON key it sets. Where a flag has no
endpoint (e.g. --socket-path), say so — those are process-management concerns the SDK/CLI
own, not the API.
# Generate the raw material: flags on one side, endpoints on the other.
./firectl --help 2>&1 | rg -- '--'
rg -n "PUT|PATCH|GET" ../firecracker-go-sdk/client/operations/*.go | rg -o '"/[a-z-]+' | sort -u
Step 5: The bisection drill — SDK bug vs. Firecracker bug
The reason this lab exists. Given any microVM misbehavior reached through the SDK or firectl, you separate the two suspects with one move:
symptom via the SDK / firectl
│
├─ reproduce with raw curl on the SAME socket?
│ ├─ YES → Firecracker (or below) owns it. The SDK only forwarded.
│ └─ NO → the SDK/firectl built a wrong request → file on the SDK repo.
│
└─ compare the SDK's request body (Step 2) to a known-good curl body
→ the field that differs is the SDK bug.
Run it for real: introduce a deliberate fault (e.g. an out-of-range MemSizeMib) through
the SDK, observe the failure, then send the same body via curl. If curl fails
identically, Firecracker's validation is doing its job and the SDK merely relayed your bad
value — not an SDK bug. If curl with a correct body succeeds where the SDK failed, the
SDK mangled the request — and now you have a precise, Firecracker-free SDK bug report.
Implementation Requirements / Deliverables
-
A Go program using firecracker-go-sdk that boots a configured microVM via
NewMachine+machine.Start. -
Proof (Step 2) that the SDK's socket answers a plain
curl GET /— i.e. it's an ordinary firecracker process. - The same microVM launched via firectl with equivalent flags.
- A complete flag/field → REST endpoint mapping table for your SDK and firectl versions.
- One worked SDK-vs-Firecracker bisection from Step 5, with the verdict and the evidence.
Troubleshooting
The Go program won't compile against the SDK
Field or constructor-option names changed between releases. Run the Prerequisites rg and
read the SDK's examples/ / *_test.go for current usage — those are kept in sync with the
code. Pin a known SDK version in go.mod if you need stability.
NewMachine succeeds but Start hangs
Usually the firecracker binary path or the kernel/rootfs path is wrong, or the socket is
stale. Confirm the process spawned (pgrep firecracker), delete any leftover socket, and
check the firecracker process's own stderr/log — the SDK surfaces FC's error but you often
need the VMM log for detail.
firectl exits immediately
Read its stderr; firectl forwards Firecracker's startup error. A bad kernel path or a
missing /dev/kvm fails here exactly as it would with raw curl — because it's the same
API underneath.
"It works with curl but not the SDK" (or vice-versa)
That is not a nuisance — it's a result. It localizes the bug to the SDK's request shaping (Step 5). Capture both bodies and diff them.
Expected Output
A microVM booted three ways — by hand with curl, programmatically with the Go SDK, and
with one firectl command — and a written mapping proving all three drive the same REST
endpoints. Plus one bisection that cleanly assigns a deliberate fault to either the SDK or
Firecracker, with the request-body evidence.
Stretch Goals
- Use the SDK's jailer integration (
JailerCfg) and reconcile what it does with the hand-built barrier from Lab I2. - Use
machine.SetMetadataand read it back from inside the guest via MMDS; confirm it's aPUT /mmdsunderneath. - Add a
PATCH-based operation (e.g. resize a rate limiter on a drive) through the SDK and confirm it maps toPATCH /drives/{id}. - Read the firecracker-containerd shim from Lab I1 and find the exact SDK calls it makes — you now understand its control plane fully.
Validation / Self-check
- The SDK is generated from what artifact, and why does that fact bound where SDK bugs can live?
- For three SDK
Configfields, name the REST endpoint and JSON key each sets. - What does
machine.Startdo, expressed as a sequence of REST calls? - Give the one-move bisection that decides "SDK bug" vs. "Firecracker bug," and why it's decisive.
- Which firectl flags map to no endpoint at all, and what do they control instead?
- Why is the SDK incapable, by construction, of doing something the REST API cannot?
- How does firectl relate to the SDK, and the SDK to the REST API?
Next: Lab I4: Bug Attribution — the keystone integration skill: is it Firecracker, the guest kernel, KVM, the host, or the orchestrator?