
DevOps 學習筆記

用 kind 學完 K8s 核心概念後, 換成 k3s 並搭配 local registry, 讓本機練習更貼近 production 的部署流程。同時釐清 K8s 網路隔離機制, 以及 production 不會用 `kubectl set image` 的原因

## 為什麼從 kind 換成 k3s

kind 學概念夠用, 但有幾個跟 production 差很遠的地方:

| 功能 | kind | k3s | Production (GKE/AKS) |
|------|------|-----|---------------------|
| Ingress controller | 沒有, 要自己裝 | 內建 Traefik | 內建 cloud LB |
| Service LoadBalancer | 拿不到外部 IP | 內建 ServiceLB, 拿得到 IP | 真的外部 IP |
| 多 Node | 可以但很少人用 | 輕鬆開 multi-node | 自動管理 |
| Local storage | 要自己設 | 內建 local-path provisioner | 自動 (Persistent Disk) |
| Image 載入 | `kind load` (非標準) | 從 registry pull (標準流程) | 從 Artifact Registry pull |

k3s 是完整的 K8s, 只是更輕量。裝完就有 Ingress 和 LoadBalancer 可以用, 不需要 `port-forward`

### 安裝與設定

```bash
# install k3s
curl -sfL https://get.k3s.io | sh -

# k3s kubeconfig is at /etc/rancher/k3s/k3s.yaml (root-only)
# copy to kubectl's default location
mkdir -p ~/.kube
sudo cp /etc/rancher/k3s/k3s.yaml ~/.kube/config
sudo chown $(id -u):$(id -g) ~/.kube/config

# verify
kubectl get nodes
```

kind 安裝完會自動設定 kubeconfig, 但 k3s 把 kubeconfig 放在 root-only 的路徑, 需要手動複製到 `~/.kube/config`, 讓一般使用者的 `kubectl` 能讀到

## Local Registry — 模擬 Production 的 Image 流程

### kind 的做法 (非標準)

```bash
# kind: transfer image from Docker daemon to kind's containerd
kind load docker-image go-api:0.0.2 --name devops-lab
```

這是 kind 專屬的指令, production 環境不存在。實際上 K8s 是從 registry pull image 的

### k3s + Local Registry (標準流程)

跑一個 local registry container, 跟 Docker Hub / GHCR / Artifact Registry 是同一種東西, 只是在本機:

```bash
# start local registry
# --name registry is the container name (can be anything)
# registry:2 is the image name + tag from Docker Hub
docker run -d -p 5050:5000 --restart always --name registry registry:2
```

注意 port mapping: registry 內部固定聽 5000, 對外可以用任意 port。`5050:5000` 代表 host 的 5050 → container 的 5000

### Build → Push → Pull 流程

```bash
# 1. build, tag points to local registry
docker buildx build -t 127.0.0.1:5050/go-api:0.0.3 .

# 2. push to registry (same as pushing to GHCR or Artifact Registry)
docker push 127.0.0.1:5050/go-api:0.0.3

# 3. verify image exists in registry
curl http://127.0.0.1:5050/v2/_catalog
# {"repositories":["go-api"]}

curl http://127.0.0.1:5050/v2/go-api/tags/list
# {"name":"go-api","tags":["0.0.3"]}
```

跟 production 的對比:

```
Local:  docker push 127.0.0.1:5050/go-api:0.0.3
GHCR:   docker push ghcr.io/ianyu93/pebble-api:latest
GCP:    docker push us-docker.pkg.dev/my-project/repo/api:abc123
```

只有目的地不同, 流程一模一樣。K8s 看到 Deployment 裡的 `image: 127.0.0.1:5050/go-api:0.0.3`, 會自己去 registry pull

### Deployment YAML

```yaml
spec:
  containers:
    - name: go-api
      image: 127.0.0.1:5050/go-api:0.0.3   # pull from registry, not local import
      ports:
        - containerPort: 8080
```

## K8s 網路隔離

### 為什麼本機連不到 Service

K8s cluster 有自己獨立的網路, 跟 host 網路是隔離的:

```
Host network (your machine)
├── can reach: 127.0.0.1, localhost
├── can reach: docker containers (via port mapping)
└── cannot reach: K8s Pod IP, Service IP, Service DNS name

K8s cluster internal network (isolated)
├── Pod IP: 10.42.0.x
├── Service IP: 10.43.x.x
└── DNS: go-api → 10.43.148.136
```

`go-api` 這個 DNS 名字只有 K8s 內部的 CoreDNS 認識, 本機的 DNS 不知道它是什麼

### 從 cluster 內部驗證 Service Discovery

```bash
# create a temporary Pod inside the cluster
kubectl run test --rm -it --image=busybox -- sh

# inside the Pod, call go-api by name
wget -qO- http://go-api:8080/healthz
# {"status":"ok","version":"0.0.3"}
```

`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

在 Pod 裡面用 `http://go-api:8080` 能連到, 是因為 Pod 在 K8s 網路裡, 可以查 CoreDNS

### Production 怎麼讓外部連進來

本機連不到 cluster 內部, 那外部使用者怎麼連?

| 方式 | 用途 |
|------|------|
| `kubectl port-forward` | 開發測試用, 不是 production 做法 |
| `Service type: LoadBalancer` | k3s / cloud 環境, 拿到真的外部 IP |
| Ingress | production 標準做法, 一個入口路由到多個 Service |

kind 拿不到 LoadBalancer IP, 只能用 `port-forward`。k3s 內建 ServiceLB, 改成 `type: LoadBalancer` 就能拿到外部 IP

## Production 部署流程 vs 練習捷徑

### 練習時的捷徑 (不要帶到 production)

```bash
# imperative: directly change image, no record
kubectl set image deployment/go-api go-api=go-api:0.0.4
```

### Production 的做法

改 YAML → commit → push → CI/CD 自動 apply:

```yaml
# k8s/deployment.yaml
spec:
  containers:
    - name: go-api
      image: 127.0.0.1:5050/go-api:0.0.4   # change this line
```

```bash
# the standard flow
git add k8s/deployment.yaml
git commit -m "bump go-api to 0.0.4"
git push
# CI/CD pipeline runs kubectl apply automatically
```

### 為什麼不用 `kubectl set image`

| | `kubectl set image` | YAML + git push |
|--|---------------------|-----------------|
| 紀錄 | 無, 誰改的都不知道 | git history 完整追蹤 |
| 回滾 | `kubectl rollout undo` | `git revert` (更可靠) |
| 審核 | 無法 review | PR review 後才 merge |
| 一致性 | cluster 狀態跟 YAML 不一致 | git = single source of truth |

這就是 GitOps 的核心 — git 裡的 YAML 永遠代表 production 的真實狀態。ArgoCD 這類工具就是監控 git repo, YAML 一變就自動 sync 到 cluster

## 從 Docker Compose 到 K8s 的對應

如果已經有跑在 EC2 + Docker Compose 的服務, 轉 K8s 時的對應關係:

| Docker Compose | K8s |
|----------------|-----|
| `services:` each service | Deployment + Service YAML |
| `ports:` | Service (ClusterIP / LoadBalancer) |
| `environment:` | ConfigMap / Secret |
| `volumes:` | PersistentVolumeClaim (PVC) |
| `depends_on:` | K8s 不管啟動順序, app 要自己 retry |
| `logging: awslogs` | GKE 內建送 Cloud Logging |
| `restart: always` | Deployment 預設行為 (reconciliation loop) |
| nginx reverse proxy | Ingress controller 取代 |

轉換時 **application code 和 Dockerfile 完全不用改**, 改的只是部署方式 — 從 Compose 換成 K8s YAML

## References

- [k3s Documentation](https://docs.k3s.io/) — k3s 安裝與設定
- [Docker Registry HTTP API V2](https://distribution.github.io/distribution/spec/api/) — registry 的 HTTP API 規格
- [Kubernetes Networking Model](https://kubernetes.io/docs/concepts/services-networking/) — K8s 網路模型
- [Kubernetes Service](https://kubernetes.io/docs/concepts/services-networking/service/) — Service types 與 service discovery
