Knative

Mental model (what you “get” when you install it)

Knative is two largely independent subsystems that happen to work well together:

Knative Serving makes HTTP-addressable workloads behave “serverless”: revisions, traffic splitting, and autoscaling (including scale-to-zero). The native unit of deployment is a container behind an HTTP port.

Knative Eventing makes events routable inside the cluster: sources produce CloudEvents, Brokers buffer/ingest, Triggers filter/route to sinks (often Knative Services). Event delivery is fundamentally HTTP POST, with retry/backoff semantics.

![important] Knative is not “functions runtime” by default; it’s “Kubernetes-native control planes + conventions” for serverless-style ops. If you want “functions from source”, that’s Knative Functions (an extra developer toolchain that produces Knative Services).

Installing Knative (Operator vs YAML) and where Helm fits

You do not need an operator. There are two common install paths:

YAML-based install is explicit and diffable: you apply the CRDs and core components for Serving/Eventing, and you own upgrades step-by-step.

# Example: install Serving v1.20.0 via upstream release YAMLs
kubectl apply -f https://github.com/knative/serving/releases/download/knative-v1.20.0/serving-crds.yaml
kubectl apply -f https://github.com/knative/serving/releases/download/knative-v1.20.0/serving-core.yaml

Operator-based install adds a reconciler that owns component lifecycle. You install the operator, then create KnativeServing / KnativeEventing custom resources to declare the desired version/config.

# Example: install Knative Operator (v1.19 docs show this pattern; adjust version as needed)
kubectl apply -f https://github.com/knative/operator/releases/download/knative-v1.19.5/operator.yaml

With the operator, you can pin versions:

apiVersion: operator.knative.dev/v1beta1
kind: KnativeServing
metadata:
  name: knative-serving
  namespace: knative-serving
spec:
  version: "1.20"

![warning] If you manage Knative via the Operator, treat Knative-owned ConfigMaps as derived state. Manual edits are typically overwritten; configuration should flow through the Operator CR (spec.config, spec.workloads, etc.) rather than imperative kubectl edit configmap ….

Helm: upstream Knative does support Helm for some installs (notably the Operator chart in some doc sets), but in practice most production setups stick to either “Operator + CRs” or “raw YAMLs” to avoid Helm-vs-reconciler ownership ambiguity.

Serving CRDs: what matters operationally

Serving has several CRDs, but one is the primary interface:

  • Service (serving.knative.dev/v1, kind Service) is the top-level object you apply.
  • Configuration is the “floating HEAD” of the rollout; updating it creates a new Revision.
  • Revision is immutable and points at a specific image (tag resolves to a digest at creation time).
  • Route programs ingress and handles traffic splitting between revisions.

The “shape” you should internalize is: Service.spec.template → stamps a new Revision; Service.spec.traffic → decides where requests go.

Key fields you tend to tune at staff-level (because they affect failure modes):

  • containerConcurrency: request parallelism per pod (drives queueing vs pod fan-out).
  • timeoutSeconds, responseStartTimeoutSeconds, idleTimeoutSeconds: separate “slow start” vs “slow stream” vs “slow total”.
  • autoscaling annotations (min/max scale, metrics mode) determine cold-start frequency vs cost.

Deploying a “function” to Knative (container-first)

If you already have a container image (your “function” is just an HTTP handler), deploy as a Knative Service:

apiVersion: serving.knative.dev/v1
kind: Service
metadata:
  name: hello
spec:
  template:
    metadata:
      annotations:
        autoscaling.knative.dev/minScale: "0" # allow scale-to-zero
        autoscaling.knative.dev/maxScale: "20"
    spec:
      containerConcurrency: 50
      timeoutSeconds: 30
      containers:
        - image: ghcr.io/knative/helloworld-go:latest
          ports:
            - containerPort: 8080
          env:
            - name: TARGET
              value: "World"

Apply it:

kubectl apply -f hello.yaml
kubectl get ksvc hello

Traffic splitting (canary) is expressed on the Service:

apiVersion: serving.knative.dev/v1
kind: Service
metadata:
  name: checkout
spec:
  template:
    spec:
      containers:
        - image: ghcr.io/acme/checkout:2.0.0
  traffic:
    - latestRevision: true
      percent: 10
    - revisionName: checkout-00012
      percent: 90

The subtle point: you are not “routing to deployments”; you’re routing to immutable revisions (or “latest ready revision”).

Deploying from source with Knative Functions (the “FaaS UX” layer)

Knative Functions gives you a source-centric workflow that builds an OCI image and deploys it as a Knative Service.

# Create a function project from a template
func create -l go hello
cd hello
 
# Deploy (build + push image + create/update ksvc)
func deploy --registry ghcr.io/<your-org>

This is operationally important: func is a developer toolchain; the runtime artifact is still a serving.knative.dev/v1 Service you can manage with kubectl/GitOps.

Eventing: Broker + Trigger (and what filtering really means)

Eventing’s core CRDs are:

  • Broker (event pool + ingress URL)
  • Trigger (subscription + attribute filter + delivery target)

Create a broker:

apiVersion: eventing.knative.dev/v1
kind: Broker
metadata:
  name: default
  namespace: default

Route all events to a Knative Service:

apiVersion: eventing.knative.dev/v1
kind: Trigger
metadata:
  name: to-processor
  namespace: default
spec:
  broker: default
  subscriber:
    ref:
      apiVersion: serving.knative.dev/v1
      kind: Service
      name: processor

Filters are on CloudEvents attributes (not event payload). This impacts schema evolution: you can safely add fields in data, but routing decisions need stable attributes like type, source, or extensions.

Send a CloudEvent to the broker from inside the cluster:

kubectl run curl --image=docker.io/curlimages/curl --rm=true --restart=Never -ti -- \
  -X POST -v \
  -H "content-type: application/json" \
  -H "ce-specversion: 1.0" \
  -H "ce-source: my/curl/command" \
  -H "ce-type: my.demo.event" \
  -H "ce-id: 0815" \
  -d '{"value":"Hello Knative"}' \
  http://broker-ingress.knative-eventing.svc.cluster.local/default/default

Event delivery knobs you’ll actually use

Broker-level delivery lets you define retry/backoff and a dead-letter sink (DLQ). This is where “at-least-once + duplicates” becomes explicit.

apiVersion: eventing.knative.dev/v1
kind: Broker
metadata:
  name: default
  namespace: default
spec:
  delivery:
    retry: 5
    backoffPolicy: exponential
    backoffDelay: "PT1S"
    deadLetterSink:
      ref:
        apiVersion: serving.knative.dev/v1
        kind: Service
        name: dlq-handler

![warning] Scale-to-zero + retries can create “thundering herd” behavior: dispatcher retries while your sink is cold-starting, causing duplicates and bursty concurrency once it comes up. The fix is usually a combination of idempotency, sensible responseStartTimeoutSeconds, and delivery backoff.

Kafka in Knative Eventing (what “using Kafka” really means)

Knative Eventing can use different Broker implementations via the immutable broker-class annotation. Kafka-backed brokers let Kafka be the durability layer while still delivering to consumers over HTTP.

apiVersion: eventing.knative.dev/v1
kind: Broker
metadata:
  name: default
  namespace: default
  annotations:
    eventing.knative.dev/broker.class: Kafka
    # Optional: bring-your-own topic
    # kafka.eventing.knative.dev/external.topic: my-topic
spec:
  config:
    apiVersion: v1
    kind: ConfigMap
    name: kafka-broker-config
    namespace: knative-eventing