DEV Community

The Cyber Sidekick
The Cyber Sidekick

Posted on

Name a container port, then expose it with a NodePort Service (CKA)

Name a container port, then expose it with a NodePort Service (CKA)

This is a two minute question. In the exam you should recognize it, answer it, and move on to something hard. Today's CKA task: give an existing webserver container a named port, then expose it with a Service that is reachable both inside the cluster and on the nodes themselves. It is three deliverables, and one of them is a single command. Let's run it on a live cluster.

🎥 Watch the video: https://www.youtube.com/watch?v=K8BCAu9Fnvw

This is a CKA Services & Networking walkthrough. Every command below is real output from a live cluster, and you can reproduce the whole thing yourself (scripts at the end).

The scenario

Here is the setup. A Deployment called catalog is already running in the storefront namespace, and its container, webserver, declares no ports at all. Three things are asked of you. Give that container a port entry: name it web, port 80, protocol TCP. Put a Service called catalog-svc in front of the Deployment, and have it target that named port rather than a raw number. And make the Service answer on each node's own address as well as inside the cluster, which is a decision about the Service type. One edit, one Service, and the right type.

  • Container 'webserver' in Deployment 'catalog' (ns 'storefront') declares no ports
  • 1. Give it a port entry: name 'web', port 80, protocol TCP
  • 2. Service 'catalog-svc' in front of it, targeting that NAMED port
  • 3. It must answer on each node's address too, so pick the right Service type
  • Budget: two to three minutes, then move on

How the ports chain together

Before we touch anything, understand what a containerPort is, because that is what this task is really testing. Declaring a containerPort does not open a port. nginx already listens on 80 whether or not the pod spec mentions it, and nothing blocks traffic to an undeclared port. What the entry gives you is documentation and, more importantly, a NAME. A Service's targetPort can then say web instead of 80, so the backend port can move without editing the Service. That is the chain: the Service has its own port, its targetPort points at the container's named port, and if the Service type is NodePort, the API server also allocates a port between 30000 and 32767 that is opened on every node in the cluster.

What you are handed

Start by reading what you were handed. In the storefront namespace there is one Deployment, catalog, with three pods Running, and no Service at all. Now read the pod template. The container is named webserver, not nginx, which is a good habit to build: read the container name out of the template instead of assuming it matches the image. And there is no ports block anywhere in it. The pods serve traffic fine right now, because the container listens on 80 regardless. What is missing is the declaration, and specifically the name the Service will need.

$ kubectl get deployments,pods,svc -n storefront
NAME                      READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/catalog   3/3     3            3           2s

NAME                          READY   STATUS    RESTARTS   AGE
pod/catalog-f9fb568c4-c4dqm   1/1     Running   0          2s
pod/catalog-f9fb568c4-hfksk   1/1     Running   0          2s
pod/catalog-f9fb568c4-skpjt   1/1     Running   0          2s

$ kubectl -n storefront get deployment catalog -o yaml | grep -A5 'containers:'
      containers:
      - image: nginx:1.27-alpine
        imagePullPolicy: IfNotPresent
        name: webserver
        resources: {}
        terminationMessagePath: /dev/termination-log
Enter fullscreen mode Exit fullscreen mode

Add the named port

Part one. Edit the Deployment in place with kubectl edit deployment catalog in the storefront namespace. Find the container named webserver and add a ports block underneath it: name web, containerPort 80, protocol TCP. Copy the shape from the Kubernetes docs if you want, the Service page has it, but watch the indentation. Ports belongs to the container, at the same level as name and image, not to the pod spec. Write and quit, and kubectl pushes the change straight back to the API server.

$ kubectl -n storefront edit deployment catalog
      containers:
      - image: nginx:1.27-alpine
        imagePullPolicy: IfNotPresent
        name: webserver
        resources: {}
        terminationMessagePath: /dev/termination-log
        terminationMessagePolicy: File

$ (after editing)
      containers:
      - image: nginx:1.27-alpine
        imagePullPolicy: IfNotPresent
        name: webserver
        ports:
        - containerPort: 80
          name: web
          protocol: TCP
        resources: {}
        terminationMessagePath: /dev/termination-log
        terminationMessagePolicy: File
Enter fullscreen mode Exit fullscreen mode

The rollout

Changing the pod template triggers a rolling update, so the Deployment replaces all three pods. Wait for rollout status to say successfully rolled out, because a half-finished rollout is how you end up verifying against a pod that does not have your change. Then read the field back. The container now carries containerPort 80, name web, protocol TCP. Part one is graded on exactly that.

$ kubectl -n storefront rollout status deployment/catalog
Waiting for deployment "catalog" rollout to finish: 1 out of 3 new replicas have been updated...
Waiting for deployment "catalog" rollout to finish: 1 out of 3 new replicas have been updated...
Waiting for deployment "catalog" rollout to finish: 1 out of 3 new replicas have been updated...
Waiting for deployment "catalog" rollout to finish: 2 out of 3 new replicas have been updated...
Waiting for deployment "catalog" rollout to finish: 2 out of 3 new replicas have been updated...
Waiting for deployment "catalog" rollout to finish: 2 out of 3 new replicas have been updated...
Waiting for deployment "catalog" rollout to finish: 2 out of 3 new replicas have been updated...
Waiting for deployment "catalog" rollout to finish: 1 old replicas are pending termination...
Waiting for deployment "catalog" rollout to finish: 1 old replicas are pending termination...
Waiting for deployment "catalog" rollout to finish: 1 old replicas are pending termination...
deployment "catalog" successfully rolled out

$ kubectl -n storefront get deployment catalog -o jsonpath='{.spec.template.spec.containers[0].ports}'
[{"containerPort":80,"name":"web","protocol":"TCP"}]
Enter fullscreen mode Exit fullscreen mode

Expose it as NodePort

Parts two and three are one command. kubectl expose deployment catalog, name it catalog-svc, port 80, target port web, protocol TCP, type NodePort. Every part of the task maps onto a flag. Target port web is the payoff for part one, since we can now reference the port by name. And expose copies the selector from the Deployment for you, which matters more than it sounds: a hand-written Service with a selector that does not match the pod labels is the classic way to end up with a Service that has no endpoints and answers nothing. Look at the result: type NodePort, port 80, and the API server allocated a node port from the 30000 range.

$ kubectl -n storefront expose deployment catalog --name=catalog-svc --port=80 --target-port=web --protocol=TCP --type=NodePort
service/catalog-svc exposed

$ kubectl get svc -n storefront
NAME          TYPE       CLUSTER-IP    EXTERNAL-IP   PORT(S)        AGE
catalog-svc   NodePort   10.96.83.91   <none>        80:31307/TCP   0s
Enter fullscreen mode Exit fullscreen mode

TargetPort + endpoints

Describe the Service and check the three lines a grader would check. TargetPort reads web, not a number, which proves the Service is following the name we added. NodePort shows the allocated port, and it is open on every node in the cluster, not just the one the pods happen to run on. And Endpoints lists all three pod IPs on port 80, which means the selector matched. An empty Endpoints line is the failure mode to look for: the Service exists, it answers nothing, and the task is marked wrong.

$ kubectl -n storefront describe svc catalog-svc
...
Type:                     NodePort
IP Family Policy:         SingleStack
IP Families:              IPv4
IP:                       10.96.83.91
IPs:                      10.96.83.91
Port:                     <unset>  80/TCP
TargetPort:               web/TCP
NodePort:                 <unset>  31307/TCP
Endpoints:                10.244.0.23:80,10.244.0.21:80,10.244.0.22:80
Session Affinity:         None
External Traffic Policy:  Cluster
Internal Traffic Policy:  Cluster
Events:                   <none>
Enter fullscreen mode Exit fullscreen mode

Curl both paths

Now prove it carries traffic, both ways. First through the ClusterIP, by name: catalog-svc.storefront resolves through cluster DNS and nginx answers. Then through the node port, using the node's own IP and the allocated port, which is the part deliverable three was really about. Same welcome page, a completely different path through the stack. One note on the lab: these curls run from a small pod inside the cluster, because on kind the node addresses are not routable from the laptop. On a real exam node you would curl the node IP and port directly from your terminal.

$ kubectl -n probe exec deploy/tester -- sh -c "curl -s --max-time 10 http://catalog-svc.storefront | grep -i '<title>'"
<title>Welcome to nginx!</title>

$ kubectl -n probe exec deploy/tester -- sh -c "curl -s --max-time 10 http://172.18.0.17:31307 | grep -i '<title>'"
<title>Welcome to nginx!</title>
Enter fullscreen mode Exit fullscreen mode

Exam tips

A few things to carry into the exam. Ports goes inside the container, not the pod spec, and the wrong indentation is the number one way to lose this one. The port NAME matters: if the task names the port web, then targetPort has to say web. Use kubectl expose instead of writing YAML, it is faster and it gets the selector right by construction. If you do write YAML, the selector must match the pod labels, and remember it is the pod template labels, not the Deployment's own name. NodePort does not replace the ClusterIP, it adds to it, so nothing else has to change. And do not set a nodePort value yourself unless a specific one is asked for, since letting the API server allocate is both faster and less likely to collide.

  • 'ports' belongs to the CONTAINER (same level as name/image), not the pod spec
  • targetPort: web references the NAME you added; that is why part one exists
  • kubectl expose --port/--target-port/--protocol/--type does parts 2 and 3 in one line
  • Hand-written YAML? the selector must match the POD TEMPLATE labels or Endpoints is empty
  • NodePort is a superset of ClusterIP; let the API server allocate the 30000-32767 port

Recap

  • Container 'webserver' now declares: name web, containerPort 80, protocol TCP
  • kubectl expose ... --target-port=web --type=NodePort created catalog-svc
  • describe svc: TargetPort web/TCP, NodePort allocated, 3 endpoints
  • Verified through the ClusterIP and through :; subscribe + dev.to writeup

Reproduce this yourself

The entire scenario is scripted on a throwaway kind cluster: https://github.com/The-Cyber-Sidekick/TCS_CKA_2026_Exam_Scenarios

git clone https://github.com/The-Cyber-Sidekick/TCS_CKA_2026_Exam_Scenarios.git
cd TCS_CKA_2026_Exam_Scenarios/learning/scenarios/scenario12-nodeport-service-expose
./setup.sh        # creates the cluster AND arms the scenario
# solve it by hand, or:
./solution.sh     # apply the answer key and verify
Enter fullscreen mode Exit fullscreen mode

If this helped, subscribe to The Cyber SideKick on YouTube for more CKA drills, and grab the newsletter at https://thecybersidekick.beehiiv.com.

Top comments (0)