Pods in Kubernetes receive replaceable IP addresses, so another workload should not connect to a Pod directly. A Service gives matching Pods a stable cluster DNS name and virtual IP, which is the usual internal entry point for an HTTP application, API, or backend listener.

A ClusterIP Service is the default Service type for traffic that stays inside the cluster. The Service selector must match the labels on the Pods, and the Service port must send traffic to the container port that the application actually listens on.

kubectl expose deployment copies the Deployment selector into the new Service and keeps the first Service object small enough to inspect. For source-controlled releases, save the resulting Service manifest with the workload YAML and review the selector before applying it to a shared namespace.

Steps to create a Kubernetes Service:

  1. Create a namespace for the Service test.
    $ kubectl create namespace app-service
    namespace/app-service created

    Use the target application namespace instead of app-service when exposing a real workload.

  2. Create a sample Deployment with two Pods and a container port.
    $ kubectl create deployment web --namespace app-service --image=nginx:1.29.5-alpine --replicas=2 --port=80
    deployment.apps/web created

    The sample Deployment gives the Service ready Pods to select. Use the existing application Deployment when the workload is already running.

  3. Wait for the Deployment rollout to finish.
    $ kubectl rollout status deployment/web --namespace app-service --timeout=180s
    Waiting for deployment "web" rollout to finish: 0 of 2 updated replicas are available...
    Waiting for deployment "web" rollout to finish: 1 of 2 updated replicas are available...
    deployment "web" successfully rolled out
  4. Expose the Deployment as a ClusterIP Service.
    $ kubectl expose deployment web --namespace app-service --name web --port=80 --target-port=80 --type=ClusterIP
    service/web exposed

    --port is the Service port that clients use. --target-port is the container port behind the Service.

  5. Inspect the Service address and selector.
    $ kubectl get service web --namespace app-service -o wide
    NAME   TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE   SELECTOR
    web    ClusterIP   10.96.194.197   <none>        80/TCP    10s   app=web

    The SELECTOR value must match labels on the Pods that should receive traffic.

  6. Check the EndpointSlice for ready Service backends.
    $ kubectl get endpointslice --namespace app-service -l kubernetes.io/service-name=web
    NAME        ADDRESSTYPE   PORTS   ENDPOINTS               AGE
    web-v2dmg   IPv4          80      10.244.0.3,10.244.0.2   10s

    An empty ENDPOINTS column usually means the selector does not match ready Pods, the Pods are not ready, or the target port does not match the workload.

  7. Start a temporary Pod that requests the Service DNS name.
    $ kubectl run service-check --namespace app-service --restart=Never --image=curlimages/curl:8.10.1 -- -sSI http://web
    pod/service-check created
  8. Wait for the temporary request Pod to complete.
    $ kubectl wait pod/service-check --namespace app-service --for=jsonpath='{.status.phase}'=Succeeded --timeout=60s
    pod/service-check condition met
  9. Read the request Pod logs to confirm the Service reaches the backend.
    $ kubectl logs service-check --namespace app-service
    HTTP/1.1 200 OK
    Server: nginx/1.29.5
    Content-Type: text/html
    ##### snipped #####

    A 200 OK response from http://web proves cluster DNS resolved the Service name and the Service routed traffic to a selected Pod.

  10. Delete the test namespace when the Service was created only for validation.
    $ kubectl delete namespace app-service
    namespace "app-service" deleted

    Skip this command for a real application namespace. Delete only the test Service, Deployment, or request Pod if the namespace contains other workloads.