Deploying a New Service on k3s (httpbin Example)
This runbook shows a practical, repeatable flow to deploy a new service to your k3s cluster using httpbin.
Purpose and Prerequisites
Section titled “Purpose and Prerequisites”Confirm Cluster Access and Context
Section titled “Confirm Cluster Access and Context”kubectl config current-contextkubectl get nodesExpected:
- Current context is
k3s. - All nodes show
Ready.
Choose a Namespace
Section titled “Choose a Namespace”Use default for quick testing, or create a dedicated namespace:
kubectl create namespace demokubectl config set-context --current --namespace=demokubectl get nsExpected:
namespace/demo created(orAlreadyExistsif rerun).demoappears in namespace list.
Step 1: Create Manifests
Section titled “Step 1: Create Manifests”Create httpbin-deployment.yaml:
apiVersion: apps/v1kind: Deploymentmetadata: name: httpbin labels: app: httpbinspec: 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: v1kind: Servicemetadata: name: httpbin labels: app: httpbinspec: type: ClusterIP selector: app: httpbin ports: - name: http port: 80 targetPort: 8080Key fields:
replicas: desired pod count.labelsandselector: must match between Deployment and Service.portvstargetPort: service port exposed in-cluster (80) forwards to container (8080).
Step 2: Apply Resources
Section titled “Step 2: Apply Resources”kubectl apply -f httpbin-deployment.yamlExpected:
deployment.apps/httpbin created(orconfiguredon re-apply).service/httpbin created(orconfigured).
Step 3: Verify Rollout Health
Section titled “Step 3: Verify Rollout Health”kubectl rollout status deployment/httpbin --timeout=120skubectl get deployment httpbinkubectl get pods -l app=httpbin -o widekubectl get svc httpbinExpected:
- 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:
kubectl describe deployment httpbinkubectl describe pods -l app=httpbinkubectl logs -l app=httpbin --tail=100kubectl get events --sort-by='.lastTimestamp'Look for:
- Image pull errors (
ImagePullBackOff,ErrImagePull). - Scheduling problems (
Insufficient, taints, node conditions). - Crash loops (
CrashLoopBackOffand container startup errors).
Step 4: Access the Service
Section titled “Step 4: Access the Service”Option A (Recommended First): Local Test With Port-Forward
Section titled “Option A (Recommended First): Local Test With Port-Forward”kubectl port-forward svc/httpbin 8080:80In another terminal:
curl -s http://127.0.0.1:8080/get | jq .Expected:
- Port-forward terminal shows
Forwarding from 127.0.0.1:8080. curlreturns JSON payload fromhttpbin.
Option B: Expose With NodePort (LAN Testing)
Section titled “Option B: Expose With NodePort (LAN Testing)”kubectl patch svc httpbin -p '{"spec":{"type":"NodePort"}}'kubectl get svc httpbin -o wideExpected:
- 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)”Check That Traefik Ingress Is Present
Section titled “Check That Traefik Ingress Is Present”kubectl get pods -n kube-system | grep -i traefikkubectl get deploy,ds -n kube-system | grep -i traefikkubectl get svc -n kube-system traefikkubectl get ingressclass traefik -o yamlHow to read typical output:
traefik-... Running— Traefik controller is up.svclb-traefik-... Running— k3s ServiceLB is publishing the TraefikLoadBalancerService on your nodes (normal for k3s).helm-install-traefik-* Completed— One-time Helm install jobs finished (expected, not an error whenCompleted).traefikService typeLoadBalancerwith node IPs — HTTP/HTTPS entry exists; published ports may show as80:30xxx/TCP,443:30xxx/TCPdepending on your install.IngressClassnamedtraefik— StandardIngressobjects can target Traefik. If you seeingressclass.kubernetes.io/is-default-class: "true"on that object, you may omitspec.ingressClassNameonIngressand it still uses Traefik; for runbooks,ingressClassName: traefikis clearer.
If grep traefik on pods returns nothing, Traefik may be disabled at install (for example --disable traefik) or replaced by another ingress controller.
Find Traefik Entrypoint IP
Section titled “Find Traefik Entrypoint IP”kubectl get svc -n kube-system traefik -o wideEXTERNAL-IP(often several node IPs) — Use these for DNS or/etc/hostsso your Ingresshostresolves to Traefik; reach Traefik on ports 80/443 unless your network requires the NodePort numbers shown inPORT(S)(for example80:30354/TCP).CLUSTER-IP— In-cluster only; not for clients outside the pod network.Endpointspod 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.
Create an Ingress for httpbin
Section titled “Create an Ingress for httpbin”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/v1kind: Ingressmetadata: name: httpbinspec: ingressClassName: traefik rules: - host: httpbin.example.test http: paths: - path: / pathType: Prefix backend: service: name: httpbin port: number: 80kubectl apply -f httpbin-ingress.yamlkubectl get ingress httpbinTest 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).
curl -I --resolve httpbin.example.test:80:10.0.0.60 http://httpbin.example.test/getcurl -s http://httpbin.example.test/get | jq .Step 5: Update and Redeploy
Section titled “Step 5: Update and Redeploy”Update Image
Section titled “Update Image”kubectl set image deployment/httpbin httpbin=mccutchen/go-httpbin:v2.15.1kubectl rollout status deployment/httpbinExpected:
- New ReplicaSet created.
- Pods restart on new image and return to
Running.
Quick Rollback
Section titled “Quick Rollback”kubectl rollout undo deployment/httpbinkubectl rollout status deployment/httpbinUse rollback if new version fails readiness or starts returning errors.
Step 6: Cleanup
Section titled “Step 6: Cleanup”kubectl delete -f httpbin-deployment.yamlIf you applied Option C, delete the Ingress as well:
kubectl delete -f httpbin-ingress.yamlExpected:
- Deployment and service deleted.
kubectl get all -l app=httpbinreturns no resources.
If you used a dedicated namespace and want full cleanup:
kubectl config set-context --current --namespace=defaultkubectl delete namespace demoExpected:
- Namespace enters
Terminating, then disappears.
k3s Notes and Common Pitfalls
Section titled “k3s Notes and Common Pitfalls”- Keep control-plane scheduling intentional. If workloads should stay on workers, use taints/affinity.
- Prefer
kubectl applywith committed manifests over ad-hockubectl 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: