From Kind to K3s: Local Registry and Production Deployment Workflow
DevOps Learning Notes
After learning K8s core concepts with kind, switching to k3s with a local registry to make local practice closer to production deployment workflows. Also clarifying K8s network isolation mechanisms and why production doesn’t use kubectl set image.
Switching to a More Complete On-Machine Runtime
kind is sufficient for learning concepts, but has several areas far from production:
| Feature | kind | k3s | Production (GKE/AKS) |
|---|---|---|---|
| Ingress controller | None, must install yourself | Built-in Traefik | Built-in cloud LB |
| Service LoadBalancer | Cannot get external IP | Built-in ServiceLB, gets IP | Real external IP |
| Multi-Node | Possible but rarely used | Easy multi-node | Automatically managed |
| Local storage | Must configure yourself | Built-in local-path provisioner | Automatic (Persistent Disk) |
| Image loading | kind load (non-standard) |
Pull from registry (standard flow) | Pull from Artifact Registry |
k3s is a full K8s, just more lightweight. After installation, Ingress and LoadBalancer are ready to use, no port-forward needed.
k3s Bootstrap Steps
|
|
kind automatically configures kubeconfig after installation, but k3s places kubeconfig in a root-only path, requiring manual copy to ~/.kube/config so a regular user’s kubectl can read it.
Private Artifact Store for On-Machine Distribution
Direct Side-Loading (Non-Standard Path)
|
|
This is a kind-specific command. It doesn’t exist in production environments. In reality, K8s pulls images from a registry.
Repository Container and Canonical Upload/Download
Run a local registry container. It’s the same thing as Docker Hub / GHCR / Artifact Registry, just on your local machine:
|
|
Note the port mapping: the registry internally always listens on 5000, and you can expose it on any external port. 5050:5000 means host’s 5050 โ container’s 5000.
Compile, Label, Upload, then Apply
|
|
Compared to production:
|
|
Only the destination differs. The workflow is identical. When K8s sees image: 127.0.0.1:5050/go-api:0.0.3 in the Deployment, it pulls from the registry automatically.
Pod Manifest Referencing the Registry
|
|
Cluster-Network Segmentation from the Host
Workload Addresses Stay Inside the Mesh
K8s cluster has its own independent network, isolated from the host network:
|
|
The DNS name go-api is only recognized by K8s’s internal CoreDNS. Your local machine’s DNS doesn’t know what it is.
Testing DNS Resolution from a Temporary Pod
|
|
Parameters for kubectl run --rm -it:
--rm: remove Pod when done (like docker run –rm)-i: interactive, connect stdin-t: allocate tty-- sh: run shell inside the Pod
Inside the Pod, http://go-api:8080 works because the Pod is in the K8s network and can query CoreDNS.
External Traffic Entry Points
If local machine can’t reach cluster internals, how do external users connect?
| Method | Use Case |
|---|---|
kubectl port-forward |
Development and testing, not a production approach |
Service type: LoadBalancer |
k3s / cloud environments, gets real external IP |
| Ingress | Production standard, single entry point routing to multiple Services |
kind can’t get LoadBalancer IP, so you can only use port-forward. k3s has built-in ServiceLB โ change to type: LoadBalancer and you get an external IP.
GitOps Deploy Versus Imperative Shortcuts
Quick-and-Dirty Commands for Local Testing
|
|
The Standard Git-Driven Pipeline
Modify YAML โ commit โ push โ CI/CD automatically applies:
|
|
|
|
Why Not Use kubectl set image
kubectl set image |
YAML + git push | |
|---|---|---|
| Record | None, no idea who changed it | git history fully traceable |
| Rollback | kubectl rollout undo |
git revert (more reliable) |
| Review | Cannot review | PR review before merge |
| Consistency | Cluster state diverges from YAML | git = single source of truth |
This is the core of GitOps โ the YAML in git always represents the true state of production. Tools like ArgoCD monitor the git repo, automatically syncing to the cluster whenever YAML changes.
Mapping Stack Definitions to Kubernetes Primitives
If you already have services running on EC2 + Docker Compose, here’s the mapping when transitioning to K8s:
| Docker Compose | K8s |
|---|---|
services: each service |
Deployment + Service YAML |
ports: |
Service (ClusterIP / LoadBalancer) |
environment: |
ConfigMap / Secret |
volumes: |
PersistentVolumeClaim (PVC) |
depends_on: |
K8s doesn’t manage startup order, app must handle retries |
logging: awslogs |
GKE built-in Cloud Logging |
restart: always |
Deployment default behavior (reconciliation loop) |
| nginx reverse proxy | Ingress controller replaces |
During migration, application code and Dockerfile need zero changes. What changes is the deployment method โ from Compose to K8s YAML.
References
- k3s Documentation โ k3s installation and configuration
- Docker Registry HTTP API V2 โ registry HTTP API specification
- Kubernetes Networking Model โ K8s networking model
- Kubernetes Service โ Service types and service discovery