logo

containerd - Envelope

In the context of containerd, an Envelope is a standardized wrapper (a Protobuf message) used to carry event data through its internal pub/sub system.

Because containerd handles many different types of events (container starts, image pulls, task exits, etc.), it needs a uniform way to "mail" these messages. The Envelope is the "mailing envelope" that contains the metadata on the outside and the specific event data on the inside.

The Structure of an Envelope

In the containerd source code (specifically in the api/events package), an Envelope typically contains these four key fields:

  1. Timestamp: Exactly when the event occurred.
  2. Namespace: The containerd namespace the event belongs to (e.g., k8s.io or default). Events are strictly isolated; a subscriber listening to the default namespace won't see events from k8s.io.
  3. Topic: A string that acts as the "address" or "category" of the event. Topics look like filesystem paths.
    • Example: /tasks/create
    • Example: /images/update
  4. Event (The Event Payload): This is an Any type (Protobuf's way of saying "any data"). This is where the actual specific details live (e.g., the ID of the container that just exited).

The Analogy: A Physical Letter

Think of a physical letter being sent through a sorting office:

  • The Envelope: Has a timestamp (postmark), a namespace (the city), and a topic (the department it's going to).
  • The Contents: Inside the envelope is the actual letter. One envelope might contain a "Birth Certificate" (Task Create), and another might contain a "Death Certificate" (Task Exit).

The sorting office (containerd's event manager) doesn't need to open the letter to know where to send it; it just looks at the Topic and Namespace on the Envelope.

Why use an Envelope?

The Envelope pattern allows containerd to be highly decoupled:

  • Uniformity: The event bus only has to understand one type of object: the Envelope. It doesn't need to know the specific details of a "Snapshot Update" vs. a "Container Delete."
  • Filtering: When you write a plugin or a tool to listen to events, you can tell containerd: "Only send me Envelopes where the topic starts with /tasks/." containerd can filter these very fast without "opening" the payload.
  • Version Independence: If containerd adds a new type of event in the future, the Envelope structure doesn't have to change. Only the data inside the Envelope changes.

How to see Envelopes in action

If you have containerd running, you can use the ctr tool to watch these envelopes fly by in real-time.

Run this in your terminal:

sudo ctr events

When you start or stop a container in another window, you will see output like this:

TIMESTAMP                    TOPIC           ATTRIBUTES
2024-05-20T10:00:00Z         /tasks/start    id=my-container, pid=1234

Each line you see here is a summarized view of an Envelope.

How to subscribe to Envelopes/Events in Go?

Use Client.Subscribe to get a channel. https://pkg.go.dev/github.com/containerd/containerd#Client.Subscribe

	// Use the containerd client to subscribe to topics
	envelopes, errs := client.Subscribe(ctx, `topic=="/tasks/create"`, `topic=="/containers/delete"`)

  // Select inside a loop:
	for {
		select {
		case <-ctx.Done():
			return nil
		case err := <-errs:
			if err != nil && err != io.EOF {
				log.ErrorContextf(ctx, "failed to subscribe: %v", err)
			}
			return err

    // Waiting for the event
		case envelope, ok := <-envelopes:
			if !ok {
				log.InfoContext(ctx, "containerd event channel closed.")
				return nil
			}
      // process the envelope/event
			processEnvelope(ctx, envelope)
		}
	}