c9s
Guides

Service exposure

Configure how Clabernetes exposes network nodes through Kubernetes Services.

This guide explains how to configure Clabernetes service exposure for your network topologies.

Overview

By default, Clabernetes creates LoadBalancer services for each node in your topology, automatically exposing common network management ports. This behavior can be customized to match your access requirements.

How exposure works

A node container does not run in its launcher pod's network namespace -- it sits on a containerlab managed docker bridge inside the pod. A node port is therefore only reachable from outside when docker publishes it, which makes the exposed port set an explicit list rather than "everything the node listens on":

  1. The default management port list (see the table below) is exposed unless disableAutoExpose: true.
  2. Anything outside that list -- a custom app on 8080, iperf3 on 5201 -- has to be named in the node's ports.
  3. For each destination port, c9s allocates a pod-side port and records the pair in the Node's status.exposedPorts. That status is the source of truth for both the node's Service and the topology the launcher hands to containerlab.

Clients always connect to the node's natural port: the Service listens on the destination port and targets the allocated pod-side port. That allocation is c9s's to make, which is why ports entries declare a destination port only.

Portable containerlab topologies

A normal containerlab ports entry publishes the port on the local Docker host. When a port is needed only so nodes can communicate through a c9s Service, use the c9s definition label instead:

topology:
  nodes:
    gnmic:
      kind: linux
      image: ghcr.io/openconfig/gnmic:latest
      labels:
        c9s.run/exposePorts: "9273/tcp,8125/udp"

The value is a comma-separated list using the same destination-port grammar as Node.spec.ports. Each entry is a destination port with an optional tcp or udp protocol. The c9s topology compiler and clabverter --emit-crs consume all entries into Node.spec.ports; the label is not copied to Kubernetes labels. Invalid entries fail compilation. Local containerlab keeps the value as an inert container label and does not publish either port on the host.

This label only declares which ports the c9s Service carries. The effective LauncherProfile still controls whether that Service is a ClusterIP, LoadBalancer, Headless, or disabled.

Exposure Options

Complete Disable (disableExpose: true)

When you don't need any Kubernetes services for your topology nodes:

apiVersion: c9s.run/v1alpha1
kind: Topology
metadata:
  name: internal-only
spec:
  expose:
    disableExpose: true
  definition:
    containerlab: |
      name: internal
      topology:
        nodes:
          srl1:
            kind: nokia_srlinux
            image: ghcr.io/nokia/srlinux:latest

Effects:

  • No services are created for any node
  • Nodes can still communicate with each other via VXLAN tunnels
  • No external access to nodes

Use cases:

  • Automated testing pipelines where nodes only need internal connectivity
  • Resource-constrained clusters where LoadBalancers are expensive
  • Security-sensitive environments

Disable Auto-Expose (disableAutoExpose: true)

Control exactly which ports are exposed:

apiVersion: c9s.run/v1alpha1
kind: Topology
metadata:
  name: minimal-ports
spec:
  expose:
    disableAutoExpose: true
  definition:
    containerlab: |
      name: minimal
      topology:
        nodes:
          srl1:
            kind: nokia_srlinux
            image: ghcr.io/nokia/srlinux:latest
            ports:
              - 22/tcp    # SSH only
              - 57400/tcp # gNMI

Each entry is the destination port -- the port the node itself listens on -- with an optional protocol. clabernetes allocates the pod-side port that carries it and records both in the Node status, so docker style host:container bindings are not used here.

Effects:

  • Only ports explicitly defined in the containerlab topology are exposed
  • Automatic port list is not added

Auto-exposed ports (when disabled, these are NOT exposed):

PortProtocolService
21TCPFTP
22TCPSSH
23TCPTelnet
80TCPHTTP
161UDPSNMP
443TCPHTTPS
830TCPNETCONF over SSH
5000TCPvrnetlab QEMU telnet
5900TCPVNC
6030TCPgNMI (Arista default)
9339TCPgNMI/gNOI
9340TCPgRIBI
9559TCPP4RT
57400TCPgNMI (Nokia default)

Service Types

LoadBalancer (Default)

External access via cloud load balancer:

spec:
  expose:
    exposeType: LoadBalancer

Characteristics:

  • Provisions a cloud LoadBalancer (or MetalLB in bare-metal clusters)
  • Each node gets an external IP address
  • Ports are accessible from outside the cluster

ClusterIP

Internal-only access within the cluster:

spec:
  expose:
    exposeType: ClusterIP

Characteristics:

  • No external IP provisioned
  • Access via service name: <topology>-<node>.<namespace>.svc.cluster.local
  • Suitable for in-cluster automation and testing

Headless

Direct pod access via DNS without load balancing:

spec:
  expose:
    exposeType: Headless

Characteristics:

  • Creates a headless service (clusterIP: None)
  • DNS queries return pod IPs directly instead of a virtual service IP
  • No load balancing or proxying by kube-proxy
  • Useful for StatefulSet-like access patterns where you need direct pod connectivity

Use cases:

  • Service discovery where clients need to connect directly to specific pods
  • Custom load balancing logic in client applications
  • Integration with external service meshes that handle their own load balancing
  • Scenarios where you need DNS-based pod discovery without Kubernetes proxying

None

No services but configuration preserved:

spec:
  expose:
    exposeType: None

Characteristics:

  • Similar to disableExpose: true but the expose configuration is preserved
  • Useful when you might want to enable services later without changing other settings

Using Management IPs

You can assign specific IPs to LoadBalancer services based on the node's management IP from your containerlab topology.

IPv4 Management IP

apiVersion: c9s.run/v1alpha1
kind: Topology
metadata:
  name: static-ips
spec:
  expose:
    exposeType: LoadBalancer
    useNodeMgmtIpv4Address: true
  definition:
    containerlab: |
      name: static
      topology:
        nodes:
          srl1:
            kind: nokia_srlinux
            image: ghcr.io/nokia/srlinux:latest
            mgmt-ipv4: 10.100.1.10  # This becomes the LoadBalancer IP
          srl2:
            kind: nokia_srlinux
            image: ghcr.io/nokia/srlinux:latest
            mgmt-ipv4: 10.100.1.11

IPv6 Management IP

spec:
  expose:
    exposeType: LoadBalancer
    useNodeMgmtIpv6Address: true

Requirements:

  • Your cluster must support the specified IP addresses
  • MetalLB or similar must have the IPs in its address pool
  • If the IP is invalid or unavailable, Kubernetes allocates an IP automatically

Use cases:

  • Consistent IP addressing across topology deployments
  • Integration with external systems expecting specific IPs
  • DNS pre-configuration

Examples Comparison

ConfigurationServices CreatedExternal AccessPort Control
DefaultLoadBalancerYesAuto + Manual
disableExpose: trueNoneNoN/A
disableAutoExpose: trueLoadBalancerYesManual only
exposeType: ClusterIPClusterIPNoAuto + Manual
exposeType: HeadlessHeadless (clusterIP: None)NoAuto + Manual
exposeType: NoneNoneNoN/A

Accessing Nodes

With LoadBalancer

# Get service IPs
kubectl get svc -l c9s.run/topologyOwner=my-topology

# SSH to node
ssh admin@<EXTERNAL-IP>

# gNMI to node
gnmic -a <EXTERNAL-IP>:57400 -u admin -p NokiaSrl1! capabilities

With ClusterIP

# From within the cluster (e.g., from a debug pod)
kubectl run debug --rm -it --image=alpine -- sh
apk add openssh-client
ssh admin@my-topology-srl1.default.svc.cluster.local

With Headless

# From within the cluster - DNS returns pod IPs directly
kubectl run debug --rm -it --image=alpine -- sh
apk add openssh-client bind-tools

# DNS lookup returns pod IP(s) instead of a virtual service IP
nslookup my-topology-srl1.default.svc.cluster.local

# Connect directly to the pod
ssh admin@my-topology-srl1.default.svc.cluster.local

With No Services

# Access via pod directly (not recommended for production)
kubectl exec -it deploy/my-topology-srl1 -- sr_cli

Best Practices

  1. Production deployments: Use exposeType: LoadBalancer with disableAutoExpose: true to expose only necessary ports

  2. CI/CD pipelines: Use disableExpose: true when nodes only need internal connectivity

  3. Development: Use default settings for convenience

  4. Security: Disable auto-expose and explicitly define only required ports

  5. Cost optimization: Use ClusterIP or Headless when external access isn't needed

  6. Service mesh integration: Use exposeType: Headless when integrating with service meshes that handle their own load balancing

On this page