Local Development
This guide walks through setting up a local development environment using kind to test the nebari-llm-serving-pack without a full Nebari deployment.
Scope of this dev path. The local
kindsetup exercises the operator, key manager, CRD, webhook, and reconciler logic against a real Kubernetes API, with Envoy Gateway and the Envoy AI Gatewayext_procextension wired up. ServedLLMModels use a mock vLLM instead of real model-serving pods, so there is no GPU inference. External-providerPassthroughModels route to the real provider, so end-to-end inference works against, for example, OpenRouter (see “Test an external provider” below). For a full GPU serving deployment use a real cluster as documented in the Installation guide.
Prerequisites
Section titled “Prerequisites”Install the following tools before proceeding:
- kind v0.20+
- kubectl
- helm v3.12+
- Docker (or compatible container runtime)
- Go 1.25+ (the key manager requires 1.25; the operator requires 1.24 - each builds with the version pinned in its own
go.mod)
1. Clone the repo
Section titled “1. Clone the repo”git clone https://github.com/nebari-dev/llm-serving-packcd llm-serving-pack2. Create the dev cluster
Section titled “2. Create the dev cluster”The dev/Makefile automates cluster creation and dependency installation:
cd devmake setupThis creates a kind cluster named llm-serving-test and installs:
- cert-manager (for webhook TLS)
- Gateway API CRDs
- Gateway API Inference Extension CRDs
- Envoy AI Gateway
- Envoy Gateway, wired with the AI Gateway
ext_procextension (dev/eg-extension-values.yaml) - The
LLMModelandPassthroughModelCRDs - Test
GatewayClassandGatewayresources
Dependency versions are pinned at the top of dev/Makefile. They move as a
set: Envoy AI Gateway v0.5.x requires Envoy Gateway v1.6.x and Gateway API
v1.4.0 (see the compatibility matrix).
The setup takes a few minutes. You can watch progress in the terminal output.
3. Build and load images
Section titled “3. Build and load images”Build the operator, key manager, and mock vLLM images, then load them into the kind cluster:
make build-imagesmake load-imagesThe mock vLLM image simulates a vLLM server for testing without a GPU. It responds to OpenAI-compatible API calls with canned responses.
4. Deploy the operator and key manager
Section titled “4. Deploy the operator and key manager”make deployThis applies the manifests in dev/manifests/ and waits for the deployments to become ready. You can also deploy them individually:
make deploy-operator # operator onlymake deploy-key-manager # key manager onlyVerify the deployments:
kubectl -n llm-operator-system get podsExpected output:
NAME READY STATUS RESTARTS AGEllm-key-manager-xxxxxxxxx-xxxxx 1/1 Running 0 30sllm-operator-xxxxxxxxx-xxxxx 1/1 Running 0 45s5. Deploy a test model
Section titled “5. Deploy a test model”Apply the test LLMModel resource, which uses the mock vLLM image:
make apply-test-modelThis creates an LLMModel named test-model in the llm-operator-system namespace. The operator reconciles it and creates the supporting resources. Per #59 all LLMModels must live in the operator’s own namespace - the validating webhook rejects anywhere else.
6. Watch reconciliation
Section titled “6. Watch reconciliation”Watch the LLMModel status update as the operator reconciles:
kubectl -n llm-operator-system get llmmodels -wYou should see the READY column transition through states as each sub-resource is created. Once all reconcilers complete, the model shows Ready.
Check the operator logs if anything looks stuck:
make logs-operator7. Verify resources
Section titled “7. Verify resources”Once the model is ready, verify the created resources:
kubectl -n llm-operator-system get allkubectl -n llm-operator-system get aigatewayrouteskubectl -n llm-operator-system get securitypoliciesThe operator creates:
- A
Deploymentrunning the mock vLLM pod - A
Servicefor the deployment - An
InferencePoolfor intelligent request scheduling AIGatewayRouteresources for external (API key) and internal (JWT) accessSecurityPolicyresources for auth enforcement (the API-key Secret they reference is co-located in this same namespace; see #59 for why)
8. Test the key manager API
Section titled “8. Test the key manager API”The key manager exposes an HTTP API for generating and revoking API keys. In the dev cluster, forward its port:
kubectl -n llm-operator-system port-forward svc/llm-key-manager 8080:8080 &List models (requires a JWT in the Authorization header or an identity cookie):
# With a fake JWT (the dev server accepts any token for testing)curl -s http://localhost:8080/api/models \ -H "Authorization: Bearer fake-jwt-token" | jq .Create an API key for the test model:
curl -s -X POST http://localhost:8080/api/keys \ -H "Authorization: Bearer fake-jwt-token" \ -H "Content-Type: application/json" \ -d '{"modelName": "test-model"}' | jq .The response includes the generated key. Keys are stored as Kubernetes Secrets in the operator namespace (defaults to llm-operator-system for the dev cluster, nebari-llm-serving-system for the chart):
kubectl -n llm-operator-system get secrets -l llm.nebari.dev/model9. Test an external provider (PassthroughModel)
Section titled “9. Test an external provider (PassthroughModel)”A PassthroughModel routes the shared endpoints to an external OpenAI-compatible
provider rather than a locally served model. This path runs end to end on kind
because the provider does the inference. These steps use OpenRouter and assume a
real OpenRouter API key.
Create the provider credential and apply the example model:
make create-openrouter-secret OPENROUTER_API_KEY=sk-or-v1-...make apply-passthrough-modelkubectl -n llm-operator-system get passthroughmodel openrouter -wOnce it reports Ready, reach it through the gateway. The external endpoint
(llm.local) uses API-key auth, so inject a client key into the api-keys Secret
to skip the key-manager:
kubectl -n llm-operator-system patch secret openrouter-api-keys --type merge \ -p '{"stringData":{"localtester":"sk-localtest-abc123"}}'
SVC=$(kubectl -n envoy-gateway-system get svc \ -l gateway.envoyproxy.io/owning-gateway-name=nebari-gateway -o name | head -1)kubectl -n envoy-gateway-system port-forward "$SVC" 8443:443 &
curl -k https://llm.local:8443/v1/chat/completions \ --resolve llm.local:8443:127.0.0.1 \ -H "Authorization: Bearer sk-localtest-abc123" \ -H "Content-Type: application/json" \ -d '{"model":"openai/gpt-4o-mini","messages":[{"role":"user","content":"hi"}]}'A curl issued immediately after the Secret patch can return 403: the key authenticates as soon as Envoy Gateway syncs the Secret, but authorization waits for the operator to re-render the model’s SecurityPolicy allow-list (typically a few seconds). Retry on a 403 before digging deeper.
The internal endpoint (llm-internal.local) always requires a real Keycloak
JWT, even when access is public, so it is not reachable on a bare kind cluster.
10. Open the key-manager UI (dev mode)
Section titled “10. Open the key-manager UI (dev mode)”The deployed key-manager runs in dev mode on kind: it bypasses auth and injects a
fixed identity (user dev, groups ["llm"]), because there is no Keycloak or
gateway OIDC layer in front of it. Forward its port and open the UI:
make ui # serves http://localhost:8080The UI loads without a login and can mint and revoke keys for any model the
dev identity’s groups grant access to. Dev mode is controlled by
LLM_DEV_MODE on the key-manager Deployment (and keyManager.devMode.enabled
in the Helm chart); it is off by default and must never be enabled in a real
deployment.
Working on the UI itself? Use
make run-devinstead of the steps above: one command brings up the cluster, three models, the port-forward, and a hot-reloading dev server. See UI Development.
11. Tail logs
Section titled “11. Tail logs”You can tail logs from either component while working:
make logs-operatormake logs-key-manager12. Run unit tests
Section titled “12. Run unit tests”Run the operator and key manager tests directly without a cluster:
cd operator && make testcd key-manager && go test ./...13. Cleanup
Section titled “13. Cleanup”When you are done, delete the kind cluster:
make teardownOr equivalently:
make cleanNext steps
Section titled “Next steps”- Read the Architecture page for the full design and CRD spec
- See
dev/manifests/test-model.yamlfor an annotated exampleLLMModel - Check the Helm chart at
charts/nebari-llm-serving/for production deployment values - For a real deployment with GPUs and OIDC, see the Quick start