Skip to content

Deploying a New Service on k3s (httpbin Example)

First PublishedLast UpdatedByAtif Alam

This runbook shows a practical, repeatable flow to deploy a new service to your k3s cluster using httpbin.

Terminal window
kubectl config current-context
kubectl get nodes

Expected:

  • Current context is k3s.
  • All nodes show Ready.

Use default for quick testing, or create a dedicated namespace:

Terminal window
kubectl create namespace demo
kubectl config set-context --current --namespace=demo
kubectl get ns

Expected:

  • namespace/demo created (or AlreadyExists if rerun).
  • demo appears in namespace list.

Create httpbin-deployment.yaml:

apiVersion: apps/v1
kind: Deployment
metadata:
name: httpbin
labels:
app: httpbin
spec:
replicas: 2
selector:
matchLabels:
app: httpbin
template:
metadata:
labels:
app: httpbin
spec:
containers:
- name: httpbin
image: mccutchen/go-httpbin:v2.15.0
ports:
- containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
name: httpbin
labels:
app: httpbin
spec:
type: ClusterIP
selector:
app: httpbin
ports:
- name: http
port: 80
targetPort: 8080

Key fields:

  • replicas: desired pod count.
  • labels and selector: must match between Deployment and Service.
  • port vs targetPort: service port exposed in-cluster (80) forwards to container (8080).
Terminal window
kubectl apply -f httpbin-deployment.yaml

Expected:

  • deployment.apps/httpbin created (or configured on re-apply).
  • service/httpbin created (or configured).
Terminal window
kubectl rollout status deployment/httpbin --timeout=120s
kubectl get deployment httpbin
kubectl get pods -l app=httpbin -o wide
kubectl get svc httpbin

Expected:

  • Rollout command ends with successfully rolled out.
  • Deployment shows READY 2/2 (or your replica count).
  • Pods are Running.
  • Service has cluster IP and port 80/TCP.

If rollout fails:

Terminal window
kubectl describe deployment httpbin
kubectl describe pods -l app=httpbin
kubectl logs -l app=httpbin --tail=100
kubectl get events --sort-by='.lastTimestamp'

Look for:

  • Image pull errors (ImagePullBackOff, ErrImagePull).
  • Scheduling problems (Insufficient, taints, node conditions).
  • Crash loops (CrashLoopBackOff and container startup errors).
Section titled “Option A (Recommended First): Local Test With Port-Forward”
Terminal window
kubectl port-forward svc/httpbin 8080:80

In another terminal:

Terminal window
curl -s http://127.0.0.1:8080/get | jq .

Expected:

  • Port-forward terminal shows Forwarding from 127.0.0.1:8080.
  • curl returns JSON payload from httpbin.

Option B: Expose With NodePort (LAN Testing)

Section titled “Option B: Expose With NodePort (LAN Testing)”
Terminal window
kubectl patch svc httpbin -p '{"spec":{"type":"NodePort"}}'
kubectl get svc httpbin -o wide

Expected:

  • Service type becomes NodePort.
  • A node port (for example 30xxx) appears.
  • Reachable at http://<node-ip>:<nodePort>/get.

Option C: Expose With Ingress (Hostname-Based)

Section titled “Option C: Expose With Ingress (Hostname-Based)”
Terminal window
kubectl get pods -n kube-system | grep -i traefik
kubectl get deploy,ds -n kube-system | grep -i traefik
kubectl get svc -n kube-system traefik
kubectl get ingressclass traefik -o yaml

How to read typical output:

  • traefik-... Running — Traefik controller is up.
  • svclb-traefik-... Running — k3s ServiceLB is publishing the Traefik LoadBalancer Service on your nodes (normal for k3s).
  • helm-install-traefik-* Completed — One-time Helm install jobs finished (expected, not an error when Completed).
  • traefik Service type LoadBalancer with node IPs — HTTP/HTTPS entry exists; published ports may show as 80:30xxx/TCP,443:30xxx/TCP depending on your install.
  • IngressClass named traefik — Standard Ingress objects can target Traefik. If you see ingressclass.kubernetes.io/is-default-class: "true" on that object, you may omit spec.ingressClassName on Ingress and it still uses Traefik; for runbooks, ingressClassName: traefik is clearer.

If grep traefik on pods returns nothing, Traefik may be disabled at install (for example --disable traefik) or replaced by another ingress controller.

Terminal window
kubectl get svc -n kube-system traefik -o wide
  • EXTERNAL-IP (often several node IPs) — Use these for DNS or /etc/hosts so your Ingress host resolves to Traefik; reach Traefik on ports 80/443 unless your network requires the NodePort numbers shown in PORT(S) (for example 80:30354/TCP).
  • CLUSTER-IP — In-cluster only; not for clients outside the pod network.
  • Endpoints pod IPs (kubectl get endpoints -n kube-system traefik) — Traefik’s internal pod ports; do not use these from your workstation.

A curl -I http://<EXTERNAL-IP>/ 404 can still mean Traefik is up but no route matches that Host/path until your Ingress exists.

Use a hostname that resolves to one EXTERNAL-IP from the Traefik Service (or add a line to /etc/hosts for testing). Point the backend at service/httpbin port 80 (the Service maps 80 to container 8080).

Create httpbin-ingress.yaml:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: httpbin
spec:
ingressClassName: traefik
rules:
- host: httpbin.example.test
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: httpbin
port:
number: 80
Terminal window
kubectl apply -f httpbin-ingress.yaml
kubectl get ingress httpbin

Test after real DNS, or add a line to /etc/hosts on your workstation: put one Traefik EXTERNAL-IP from kubectl get svc -n kube-system traefik -o wide, a space, then the exact hostname from your Ingress rule (spec.rules[].host, for example httpbin.example.test). Example: 10.0.0.60 httpbin.example.test (no http:// or path). Some browsers may ignore /etc/hosts if Secure DNS is enabled; curl and Safari usually follow it.

To skip editing hosts, use curl --resolve with the same hostname, port 80, and that EXTERNAL-IP (replace 10.0.0.60 in the command below if yours differs).

Terminal window
curl -I --resolve httpbin.example.test:80:10.0.0.60 http://httpbin.example.test/get
curl -s http://httpbin.example.test/get | jq .
Terminal window
kubectl set image deployment/httpbin httpbin=mccutchen/go-httpbin:v2.15.1
kubectl rollout status deployment/httpbin

Expected:

  • New ReplicaSet created.
  • Pods restart on new image and return to Running.
Terminal window
kubectl rollout undo deployment/httpbin
kubectl rollout status deployment/httpbin

Use rollback if new version fails readiness or starts returning errors.

Terminal window
kubectl delete -f httpbin-deployment.yaml

If you applied Option C, delete the Ingress as well:

Terminal window
kubectl delete -f httpbin-ingress.yaml

Expected:

  • Deployment and service deleted.
  • kubectl get all -l app=httpbin returns no resources.

If you used a dedicated namespace and want full cleanup:

Terminal window
kubectl config set-context --current --namespace=default
kubectl delete namespace demo

Expected:

  • Namespace enters Terminating, then disappears.
  • Keep control-plane scheduling intentional. If workloads should stay on workers, use taints/affinity.
  • Prefer kubectl apply with committed manifests over ad-hoc kubectl run.
  • Always verify context before applying: kubectl config current-context.
  • When debugging unexpected auth failures, compare local kubeconfig with /etc/rancher/k3s/k3s.yaml.

Related: