DevOpsDocker Swarm và k3s, phần 8/8

GitOps cho 12 cửa hàng: Kustomize overlay, Flux và bước CI bắt cấu hình lệch

Cập nhật tay 12 máy k3s để ba cửa hàng vẫn chạy bản cũ cả tuần mà không ai biết. Một repo Kustomize với 12 overlay, Flux kéo về từng máy, và một bước CI so 12 bản render chỉ cho phép khác nhau ở những trường đã khai báo.

Mục lục
  1. 1. Vấn đề: cập nhật tay 12 máy, ba cửa hàng vẫn chạy bản cũ
  2. 2. Mục đích: trạng thái của 12 cửa hàng nằm trong Git, lệch thì CI chặn
  3. 3. Cơ sở lý thuyết: Git giữ trạng thái mong muốn, agent trên từng máy kéo về và sửa lệch
  4. 4. Cách giải quyết: Flux trên từng máy, 12 overlay, CI so bản render
  5. 5. Cách cài đặt: repo mẫu, Flux Kustomization và công cụ so render
  6. 6. Chứng minh: 0 vi phạm với repo đúng, 3/3 trường bị bắt khi sửa tay ch09
  7. 7. Kết luận
  8. Đọc tiếp
  9. Nguồn

Đọc nhanh

  • Vấn đề: Cập nhật BanHang.CuaHang bằng tay trên 12 máy k3s mất hai buổi tối, và một tuần sau ba cửa hàng vẫn chạy bản cũ mà không ai biết.
  • Cách giải: Một repo Git với base, overlay vòng phát hành, component cỡ và 12 overlay cửa hàng; Flux trên từng máy kéo thư mục của mình và trả lại thay đổi tay.
  • Chứng minh: Chưa dựng cụm thật; 12 overlay qua kubeconform với Kubernetes 1.36.5, bước so render báo 0 vi phạm rồi bắt đủ 3 trường sửa tay ở ch09.
  • Trong .NET: Bước so render là file-based app .NET 10 dùng YamlDotNet 18.1.0, trả exit code 1 cho CI khi một trường lệch ngoài khai báo.

1. Vấn đề: cập nhật tay 12 máy, ba cửa hàng vẫn chạy bản cũ

Bản 2.1.0 của BanHang.CuaHang sửa cách chia giảm giá combo vào từng dòng hóa đơn: bản 2.0.3 làm tròn từng dòng nên tổng giảm có thể lệch 1 đồng, lỗi mà bài Chia tiền không lệch một đồng mô tả. Tối 21 và 22/9/2026, sau giờ đóng cửa, đội IT trụ sở SSH vào từng máy k3s-ch01 … k3s-ch12, sửa manifest và chạy kubectl apply.

Ngày 28/9, kế toán đối soát tuần thấy hóa đơn combo của ch05, ch09 và ch11 vẫn chia theo cách cũ. ch05 và ch11 mất mạng đúng tối cập nhật nên bị bỏ qua. ch09 đã lên 2.1.0, nhưng hôm sau kỹ thuật viên tự quay về 2.0.3 vì lỗi máy in hóa đơn, tăng memory limit lên 1Gi và đặt DongBo__ChuKyGiay=5 để theo dõi đồng bộ. Không thay đổi nào được ghi lại.

Muốn biết 12 cửa hàng đang chạy gì, hôm nay vẫn phải SSH vào 12 máy. Bản 2.2.0 sắp phát hành, và chủ hệ thống muốn chạy thử ở hai cửa hàng trước rồi mới đưa lên mười nơi còn lại.

2. Mục đích: trạng thái của 12 cửa hàng nằm trong Git, lệch thì CI chặn

  • 12/12 overlay render được và qua kubeconform -strict với Kubernetes 1.36.5, bản Kubernetes của k3s v1.36.5+k3s1 trên kênh stable.
  • Công cụ so 12 bản render chỉ thấy khác nhau theo ba chiều đã khai báo: mã và tên theo cửa hàng, memory theo cỡ, image theo vòng phát hành. Repo đúng cho 0 vi phạm. Sửa tay ch09 như tuần trước cho 3/3 trường bị bắt và exit code 1.
  • Đưa hai cửa hàng canary lên 2.2.0 là sửa 1 dòng trong Git, và đúng 2/12 bản render đổi.
  • Ngoài phạm vi: chạy Flux trên cụm thật, Secret bhpay-api-key-v1 (tạo ngoài Git), dữ liệu offline (bài 7), cách chọn probe và limit (bài 5).

3. Cơ sở lý thuyết: Git giữ trạng thái mong muốn, agent trên từng máy kéo về và sửa lệch

Bốn nguyên tắc của GitOps

OpenGitOps 1.0.0 định nghĩa GitOps bằng bốn nguyên tắc. Cách cập nhật ở mục 1 vi phạm cả bốn:

Nguyên tắc Nghĩa Ở mục 1
Declarative Trạng thái mong muốn là dữ liệu, không phải chuỗi lệnh Gõ lệnh trên từng máy
Versioned and Immutable Mỗi trạng thái là một phiên bản bất biến, có lịch sử Thay đổi ở ch09 không để lại dấu vết
Pulled Automatically Agent tự kéo trạng thái mong muốn Trụ sở đẩy; máy mất mạng thì lỡ
Continuously Reconciled Agent liên tục so thật với mong muốn và sửa Không ai so, lệch tồn tại cả tuần

Kustomize: base, overlay và component

kustomize build <thư mục> nạp các resource mà kustomization.yaml trỏ tới, rồi chạy các transformer của tầng đó: đặt namespace, thêm nhãn, đổi tag image, áp patch. Resource có thể là thư mục chứa kustomization khác, nên các tầng xếp chồng: tầng ngoài biến đổi tiếp kết quả của tầng trong, và đầu ra là YAML thuần.

Repo xếp ba tầng. base là bộ manifest BanHang.CuaHang của bài 4, giữ nguyên. Overlay vòng vong/on-dinh và vong/canary trỏ tới base và chỉ đặt tag image. Overlay cuahang/chNN trỏ tới đúng một vòng, gắn nhãn, ghi mã và tên cửa hàng vào ConfigMap. Cỡ cửa hàng là Component (kind: Component, có từ kustomize 3.7.0): kustomization dùng lại được, overlay nào cần thì gọi, ở đây để patch memory.

flowchart TB
  B["base của bài 4: Deployment, Service, Ingress, ConfigMap, PVC, HPA, CronJob"]
  CO["Component co-nho, co-vua, co-lon: memory"]
  B --> OD["vong/on-dinh: tag 2.1.0"]
  B --> CA["vong/canary: tag 2.2.0"]
  OD --> C1["10 overlay cửa hàng còn lại"]
  CA --> C3["cuahang/ch03, cuahang/ch07"]
  CO -.-> C1
  CO -.-> C3
  C1 -->|"mỗi máy kéo thư mục của mình"| F1["Flux trên 10 máy k3s tương ứng"]
  C3 -->|"mỗi máy kéo thư mục của mình"| F3["Flux trên k3s-ch03, k3s-ch07"]

Vòng là overlay trung gian vì mỗi cửa hàng thuộc đúng một vòng, nên không thể vô tình nhận hai tag. Cỡ là component vì nó độc lập với vòng.

Hai chi tiết quyết định cách so ở mục 6. Base sinh ConfigMap cuahang-cauhinh bằng configMapGenerator; overlay ghi mã, tên cửa hàng bằng generator cùng tên với behavior: merge. Tên sinh ra có hậu tố hash theo nội dung, như cuahang-cauhinh-g5tkc86tdm, và kustomize sửa mọi tham chiếu theo. Biến môi trường lấy từ ConfigMap qua envFrom không tự cập nhật, theo tài liệu Kubernetes; nhờ hash, đổi cấu hình là đổi pod template, nên Deployment tự rollout.

Trường labels chỉ gắn nhãn vào metadata.labels, không vào selector. commonLabels, đã deprecated, gắn cả vào selector, mà selector của Deployment apps/v1 không đổi được sau khi tạo; chuyển ch03 từ canary sang ổn định khi đó làm apply thất bại.

Flux kéo, áp và sửa lệch

Flux chạy dưới dạng controller ngay trong cụm. source-controller lấy repo theo đối tượng GitRepository mỗi interval, mặc định 1 phút khi tạo bằng flux bootstrap. kustomize-controller build thư mục ghi trong đối tượng Kustomization rồi áp bằng server-side apply. Revision của nguồn đổi thì Kustomization được xử lý ngay, không chờ interval của nó.

Mỗi interval của Kustomization, controller build và áp lại, "correcting any existing drift" theo tài liệu Flux: kubectl edit hay kubectl set image trên máy bị trả về đúng như Git. prune: true xóa object mà revision mới không còn. wait: true chờ mọi object sẵn sàng, với Deployment là rollout xong.

Gắn lại mục 1. Máy tự kéo, nên ch05 và ch11 mất mạng chỉ nhận chậm chứ không bị bỏ qua. Thay đổi tay kiểu ch09 trên cụm bị trả lại trong một interval. Còn một lỗ hổng: thay đổi tay đi vào chính Git thì Flux áp đúng như vậy. Bước so render ở mục 4 bịt lỗ hổng này.

4. Cách giải quyết: Flux trên từng máy, 12 overlay, CI so bản render

Cách Ưu Nhược Khi nào dùng
SSH và kubectl apply từng máy Không cần công cụ 12 lần thao tác, không lịch sử, máy mất mạng bị sót Một hai máy thử
Script trụ sở lặp kubectl apply qua 12 kubeconfig Một lệnh Phải vào được API server từng máy; không ai sửa lệch Mạng ổn định, ít thay đổi
Argo CD ở trụ sở, ApplicationSet sinh 12 Application Giao diện chung cho 12 cửa hàng Hub gọi API server từng máy; mất kết nối là ngừng đồng bộ Cụm có đường ổn định về hub
Flux trong từng máy, kéo overlay của mình Chỉ cần kết nối ra; tự sửa lệch Không có giao diện; 12 bản Flux phải nâng cấp Máy biên, mạng chập chờn

Hai ứng viên GitOps, phiên bản ổn định tra ngày 4/10/2026:

Tiêu chí Flux v2.9.6 Argo CD v3.5.3
Mô hình Mỗi cụm tự kéo Hub gọi URL API server của cụm, lưu trong Secret; bản kéo argocd-agent còn ở 0.10.0
Cài trên máy cửa hàng 2 Deployment (source-controller, kustomize-controller); requests 50m CPU/64Mi và 100m CPU/64Mi Bản core: 4 workload (application-controller, repo-server, redis, applicationset-controller), không đặt requests
Sửa thay đổi tay trên cụm Mặc định, mỗi interval Chỉ khi bật selfHeal
Xóa object bị bỏ khỏi Git prune: true Tắt mặc định, bật bằng prune
Giao diện Không kèm Có, trừ bản core

Số requests đọc từ flux install --export v2.9.6 và core-install.yaml v3.5.3. Requests là mức đặt chỗ khi lập lịch, không phải mức dùng thật.

Bài chọn Flux. Máy cửa hàng nằm sau NAT, đường truyền chập chờn, nên mô hình kéo khớp hơn một hub phải gọi vào 12 API server. Flux sửa lệch mà không cần bật thêm, và chỉ cần hai controller trên máy 8 GB. Cái giá là không có giao diện: trạng thái 12 máy phải gom về trụ sở bằng cách khác.

Các bước:

  1. Repo theo hình ở mục 3, thêm clusters/k3s-chNN chứa file Flux của từng máy.
  2. Mỗi máy chạy flux bootstrap git --path=clusters/k3s-chNN --components=source-controller,kustomize-controller một lần.
  3. Mỗi pull request chạy CI: render 12 overlay, kubeconform, so 12 bản render.
  4. Phát hành theo vòng như hình dưới. Quay lại là git revert commit đổi tag.
flowchart TD
  PR["PR: vong/canary lên 2.2.0"] --> CI["CI: render 12, kubeconform, so render"]
  CI -->|"đỏ"| X["Không merge"]
  CI -->|"xanh"| M["Merge vào main"]
  M --> K["Flux ở ch03, ch07 kéo trong tối đa 1 phút, rollout"]
  K --> Q["Quan sát một ngày bán"]
  Q -->|"ổn"| P["PR: vong/on-dinh lên 2.2.0"]
  Q -->|"lỗi"| R["git revert"]

5. Cách cài đặt: repo mẫu, Flux Kustomization và công cụ so render

Công cụ, bản tra ngày 4/10/2026: kustomize v5.8.2, kubeconform v0.8.0, Flux CLI v2.9.6, .NET SDK 10.0.401 (runtime 10.0.12), gói YamlDotNet 18.1.0. Base lấy nguyên từ bài 4, gồm cả PVC, HPA 2–3 pod và CronJob sao lưu; Ingress dùng Traefik v3.7.13 đi kèm k3s v1.36.5+k3s1. Deployment không ghi replicas, nên Flux không giành trường này với HPA.

Overlay của một cửa hàng chỉ nói thuộc vòng nào, cỡ nào, là cửa hàng nào. Đây là cuahang/ch03/kustomization.yaml:

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
  - ../../vong/canary
components:
  - ../../thanh-phan/co-vua
labels:
  - pairs:
      banhang.local/cua-hang: ch03
configMapGenerator:
  - name: cuahang-cauhinh
    behavior: merge
    literals:
      - CuaHang__Ma=CH03
      - CuaHang__Ten=BanHang Long Biên

vong/canary chỉ trỏ tới base, gắn nhãn banhang.local/vong: canary và đặt newTag: 2.2.0 trong images; transformer này đổi cả ba chỗ dùng image: container chính, init container khoi-phuc và CronJob sao lưu. vong/on-dinh giống hệt với tag 2.1.0. App đọc mã cửa hàng bằng builder.Configuration["CuaHang:Ma"], vì __ trong tên biến môi trường thành : trong cấu hình .NET.

Base của bài 4, component co-vua và vong/canary: toàn bộ fileYAML · 368 dòng
# base/kustomization.yaml
# Base của BanHang.CuaHang cho k3s v1.36.5+k3s1 (Kubernetes 1.36.5).
# Overlay của từng cửa hàng thường chỉ đổi:
#   - ConfigMap cuahang-cauhinh: configMapGenerator với behavior: merge, khóa CuaHang__Ma và CuaHang__Ten;
#   - tag image registry.banhang.local/banhang/cuahang (mặc định 2.1.0) bằng images/newTag.
# Secret bhpay-api-key-v1 (khóa api-key) không nằm trong Git: tạo riêng trên từng cụm.
# Kiểm: kubectl kustomize . > render.yaml, rồi kubeconform -strict -kubernetes-version 1.36.5 render.yaml.
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: banhang
resources:
  - namespace.yaml
  - pvc.yaml
  - deployment.yaml
  - service.yaml
  - ingress.yaml
  - hpa.yaml
  - cronjob-sao-luu.yaml
# Tên sinh ra có hậu tố hash theo nội dung: đổi cấu hình là đổi pod template, Deployment tự rollout.
configMapGenerator:
  - name: cuahang-cauhinh
    options:
      labels:
        app.kubernetes.io/name: cuahang
        app.kubernetes.io/part-of: banhang
    literals:
      - CuaHang__Ma=CHUA-DAT
      - CuaHang__Ten=CHUA-DAT
      - TruSo__Url=https://api.banhang.local
      - DongBo__ChuKyGiay=30
      - ConnectionStrings__CuaHang=Data Source=/data/cuahang.db
---
# base/namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: banhang
  labels:
    app.kubernetes.io/part-of: banhang
---
# base/pvc.yaml
# Dữ liệu offline của cửa hàng (SQLite). local-path của k3s tạo PV kiểu local,
# gắn nodeAffinity kubernetes.io/hostname vào đúng node lúc tạo.
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: cuahang-data
  namespace: banhang
  labels:
    app.kubernetes.io/name: cuahang
    app.kubernetes.io/part-of: banhang
spec:
  accessModes:
    - ReadWriteOnce
  storageClassName: local-path
  resources:
    requests:
      storage: 2Gi
---
# base/deployment.yaml
# BanHang.CuaHang: backend bán tại quầy, bán được khi mất mạng, đồng bộ về trụ sở.
# Không ghi spec.replicas: HPA (hpa.yaml) giữ số pod trong khoảng 2-3.
apiVersion: apps/v1
kind: Deployment
metadata:
  name: cuahang
  namespace: banhang
  labels:
    app.kubernetes.io/name: cuahang
    app.kubernetes.io/part-of: banhang
spec:
  revisionHistoryLimit: 5
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
  selector:
    matchLabels:
      app.kubernetes.io/name: cuahang
  template:
    metadata:
      labels:
        app.kubernetes.io/name: cuahang
        app.kubernetes.io/part-of: banhang
    spec:
      terminationGracePeriodSeconds: 30   # > preStop 5 s + ShutdownTimeout 15 s
      securityContext:
        runAsNonRoot: true
        runAsUser: 1654                    # user "app" của image .NET từ bản 8
        runAsGroup: 1654
        fsGroup: 1654
        seccompProfile:
          type: RuntimeDefault
      initContainers:
        # Volume trống (máy mới, PVC mới) thì chép bản sao lưu mới nhất vào trước khi app chạy.
        - name: khoi-phuc
          image: registry.banhang.local/banhang/cuahang:2.1.0
          args: ["khoi-phuc", "--du-lieu", "/data", "--sao-luu", "/sao-luu"]
          envFrom:
            - configMapRef:
                name: cuahang-cauhinh
          resources:
            requests:
              cpu: 100m
              memory: 128Mi
            limits:
              memory: 256Mi
          securityContext:
            allowPrivilegeEscalation: false
            readOnlyRootFilesystem: true
            capabilities:
              drop: ["ALL"]
          volumeMounts:
            - name: data
              mountPath: /data
            - name: sao-luu
              mountPath: /sao-luu
              readOnly: true
            - name: tmp
              mountPath: /tmp
      containers:
        - name: cuahang
          image: registry.banhang.local/banhang/cuahang:2.1.0
          ports:
            - name: http
              containerPort: 8080
          envFrom:
            - configMapRef:
                name: cuahang-cauhinh
          resources:
            requests:
              cpu: 500m
              memory: 512Mi
            limits:
              cpu: "1"
              memory: 512Mi      # GC tự đặt heap hard limit 75% = 384 MiB
          startupProbe:          # tối đa 24 x 5 s = 120 s để khởi động
            httpGet:
              path: /healthz/live
              port: http
            periodSeconds: 5
            timeoutSeconds: 2
            failureThreshold: 24
          livenessProbe:         # không kiểm phụ thuộc nào
            httpGet:
              path: /healthz/live
              port: http
            periodSeconds: 10
            timeoutSeconds: 2
            failureThreshold: 3
          readinessProbe:        # SQLite cục bộ; mất kết nối trụ sở chỉ là Degraded
            httpGet:
              path: /healthz/ready
              port: http
            periodSeconds: 5
            timeoutSeconds: 2
            failureThreshold: 2
          lifecycle:
            preStop:
              sleep:
                seconds: 5
          securityContext:
            allowPrivilegeEscalation: false
            readOnlyRootFilesystem: true
            capabilities:
              drop: ["ALL"]
          volumeMounts:
            - name: data
              mountPath: /data
            - name: bhpay-api-key
              mountPath: /run/secrets
              readOnly: true
            - name: tmp
              mountPath: /tmp
      volumes:
        - name: data
          persistentVolumeClaim:
            claimName: cuahang-data
        - name: sao-luu                    # ổ thứ hai của máy, gắn sẵn tại /mnt/sao-luu
          hostPath:
            path: /mnt/sao-luu
            type: Directory
        - name: bhpay-api-key              # Secret tạo ngoài Git, xem kustomization.yaml
          secret:
            secretName: bhpay-api-key-v1
            items:
              - key: api-key
                path: BHPay__ApiKey
        - name: tmp
          emptyDir:
            sizeLimit: 64Mi
---
# base/service.yaml
apiVersion: v1
kind: Service
metadata:
  name: cuahang
  namespace: banhang
  labels:
    app.kubernetes.io/name: cuahang
    app.kubernetes.io/part-of: banhang
spec:
  type: ClusterIP
  selector:
    app.kubernetes.io/name: cuahang
  ports:
    - name: http
      port: 80
      targetPort: http
---
# base/ingress.yaml
# Traefik đi kèm k3s nhận cổng 80/443 của máy qua ServiceLB.
# Router của mỗi cửa hàng phân giải cuahang.banhang.local về máy k3s-chNN trong LAN.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: cuahang
  namespace: banhang
  labels:
    app.kubernetes.io/name: cuahang
    app.kubernetes.io/part-of: banhang
spec:
  ingressClassName: traefik
  rules:
    - host: cuahang.banhang.local
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: cuahang
                port:
                  name: http
---
# base/hpa.yaml
# HPA giữ 2 đến 3 pod, nhắm 70% CPU request (70% của 500m = 350m mỗi pod).
# 3 pod x limit 1 CPU chừa 1 nhân của máy mini PC 4 nhân cho k3s, Traefik và hệ điều hành.
# Deployment không ghi spec.replicas để HPA và GitOps không giành nhau trường này.
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: cuahang
  namespace: banhang
  labels:
    app.kubernetes.io/name: cuahang
    app.kubernetes.io/part-of: banhang
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: cuahang
  minReplicas: 2
  maxReplicas: 3
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 70
  behavior:
    scaleDown:
      stabilizationWindowSeconds: 300
---
# base/cronjob-sao-luu.yaml
# Sao lưu SQLite mỗi 15 phút sang ổ thứ hai (/mnt/sao-luu), giữ 96 bản (24 giờ),
# rồi thử đẩy bản mới nhất về trụ sở. Nhãn khác "cuahang" để Service không chọn pod của Job.
apiVersion: batch/v1
kind: CronJob
metadata:
  name: cuahang-sao-luu
  namespace: banhang
  labels:
    app.kubernetes.io/name: cuahang-sao-luu
    app.kubernetes.io/part-of: banhang
spec:
  schedule: "*/15 * * * *"
  timeZone: Asia/Ho_Chi_Minh
  concurrencyPolicy: Forbid
  startingDeadlineSeconds: 300
  successfulJobsHistoryLimit: 3
  failedJobsHistoryLimit: 5
  jobTemplate:
    spec:
      backoffLimit: 2
      activeDeadlineSeconds: 600
      template:
        metadata:
          labels:
            app.kubernetes.io/name: cuahang-sao-luu
            app.kubernetes.io/part-of: banhang
        spec:
          restartPolicy: OnFailure
          securityContext:
            runAsNonRoot: true
            runAsUser: 1654
            runAsGroup: 1654
            fsGroup: 1654
            seccompProfile:
              type: RuntimeDefault
          containers:
            - name: sao-luu
              image: registry.banhang.local/banhang/cuahang:2.1.0
              args: ["sao-luu", "--du-lieu", "/data", "--sao-luu", "/sao-luu", "--giu", "96"]
              envFrom:
                - configMapRef:
                    name: cuahang-cauhinh
              resources:
                requests:
                  cpu: 100m
                  memory: 128Mi
                limits:
                  memory: 256Mi
              securityContext:
                allowPrivilegeEscalation: false
                readOnlyRootFilesystem: true
                capabilities:
                  drop: ["ALL"]
              volumeMounts:
                - name: data              # cùng PVC với Deployment: RWO cho phép vì cùng node
                  mountPath: /data
                - name: sao-luu
                  mountPath: /sao-luu
                - name: tmp
                  mountPath: /tmp
          volumes:
            - name: data
              persistentVolumeClaim:
                claimName: cuahang-data
            - name: sao-luu
              hostPath:
                path: /mnt/sao-luu
                type: Directory
            - name: tmp
              emptyDir:
                sizeLimit: 64Mi
---
# thanh-phan/co-vua/kustomization.yaml (co-nho: 384Mi, co-lon: 768Mi)
apiVersion: kustomize.config.k8s.io/v1alpha1
kind: Component
labels:
  - pairs:
      banhang.local/co: vua
patches:
  - target:
      kind: Deployment
      name: cuahang
    patch: |-
      - op: replace
        path: /spec/template/spec/containers/0/resources/requests/memory
        value: 512Mi
      - op: replace
        path: /spec/template/spec/containers/0/resources/limits/memory
        value: 512Mi
---
# vong/canary/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
  - ../../base
labels:
  - pairs:
      banhang.local/vong: canary
images:
  - name: registry.banhang.local/banhang/cuahang
    newTag: 2.2.0

Memory theo cỡ là số minh họa: 384Mi, 512Mi như base, 768Mi; bài 5 nói cách chọn limit cho heap .NET. Overlay và file Flux của 12 máy sinh bằng một script:

tao-repo.ps1: sinh component cỡ, overlay vòng, 12 overlay cửa hàng và 12 Flux KustomizationPowerShell · 107 dòng
# Windows PowerShell 5.1: lưu file UTF-8 có BOM để giữ dấu tiếng Việt.
$g = $PSScriptRoot
$utf8 = New-Object System.Text.UTF8Encoding($false)
function Ghi($path, $text) {
  $full = Join-Path $g $path
  New-Item -ItemType Directory -Force (Split-Path $full) | Out-Null
  [IO.File]::WriteAllText($full, ($text -replace "`r`n", "`n"), $utf8)
}

# Cỡ cửa hàng: memory request = limit của container cuahang (số minh họa; base là 512Mi).
# CPU giữ như base: request 500m cho HPA, limit 1.
$co = [ordered]@{ 'nho' = '384Mi'; 'vua' = '512Mi'; 'lon' = '768Mi' }
foreach ($ten in $co.Keys) {
  Ghi "thanh-phan/co-$ten/kustomization.yaml" @"
apiVersion: kustomize.config.k8s.io/v1alpha1
kind: Component
labels:
  - pairs:
      banhang.local/co: $ten
patches:
  - target:
      kind: Deployment
      name: cuahang
    patch: |-
      - op: replace
        path: /spec/template/spec/containers/0/resources/requests/memory
        value: $($co[$ten])
      - op: replace
        path: /spec/template/spec/containers/0/resources/limits/memory
        value: $($co[$ten])

"@
}

# Vòng phát hành: overlay trung gian, mỗi vòng một tag. Commit đầu: cả hai 2.1.0.
$vong = [ordered]@{ 'on-dinh' = '2.1.0'; 'canary' = '2.1.0' }
foreach ($ten in $vong.Keys) {
  Ghi "vong/$ten/kustomization.yaml" @"
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
  - ../../base
labels:
  - pairs:
      banhang.local/vong: $ten
images:
  - name: registry.banhang.local/banhang/cuahang
    newTag: $($vong[$ten])

"@
}

# 12 cửa hàng: mã, tên, cỡ, vòng.
$cuaHang = @(
  @('ch01', 'BanHang Cầu Giấy',   'lon', 'on-dinh'),
  @('ch02', 'BanHang Hoàn Kiếm',  'lon', 'on-dinh'),
  @('ch03', 'BanHang Long Biên',  'vua', 'canary'),
  @('ch04', 'BanHang Hải Phòng',  'vua', 'on-dinh'),
  @('ch05', 'BanHang Hải Châu',   'lon', 'on-dinh'),
  @('ch06', 'BanHang Huế',        'nho', 'on-dinh'),
  @('ch07', 'BanHang Nha Trang',  'vua', 'canary'),
  @('ch08', 'BanHang Quận 1',     'lon', 'on-dinh'),
  @('ch09', 'BanHang Thủ Đức',    'vua', 'on-dinh'),
  @('ch10', 'BanHang Gò Vấp',     'vua', 'on-dinh'),
  @('ch11', 'BanHang Cần Thơ',    'nho', 'on-dinh'),
  @('ch12', 'BanHang Biên Hòa',   'nho', 'on-dinh')
)
foreach ($ch in $cuaHang) {
  $ma, $tenCh, $c, $v = $ch
  Ghi "cuahang/$ma/kustomization.yaml" @"
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
  - ../../vong/$v
components:
  - ../../thanh-phan/co-$c
labels:
  - pairs:
      banhang.local/cua-hang: $ma
configMapGenerator:
  - name: cuahang-cauhinh
    behavior: merge
    literals:
      - CuaHang__Ma=$($ma.ToUpper())
      - CuaHang__Ten=$tenCh

"@
  Ghi "clusters/k3s-$ma/cuahang.yaml" @"
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
  name: cuahang
  namespace: flux-system
spec:
  interval: 10m
  retryInterval: 2m
  timeout: 3m
  path: ./cuahang/$ma
  prune: true
  wait: true
  sourceRef:
    kind: GitRepository
    name: flux-system

"@
}
"ok"

flux bootstrap tạo GitRepository tên flux-system và một Kustomization trỏ vào clusters/k3s-ch03. Trong thư mục đó, file dưới bảo Flux của máy k3s-ch03 áp overlay cuahang/ch03:

apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
  name: cuahang
  namespace: flux-system
spec:
  interval: 10m       # mỗi 10 phút build, áp lại, trả lại thay đổi tay
  retryInterval: 2m
  timeout: 3m
  path: ./cuahang/ch03
  prune: true         # object bỏ khỏi Git thì xóa trên máy
  wait: true          # chờ Deployment rollout xong mới báo Ready
  sourceRef:
    kind: GitRepository
    name: flux-system

Công cụ so 12 bản render

SoCuaHang.cs trải mỗi file render thành cặp đường dẫn và giá trị lá, như Deployment/cuahang .spec.template.spec.containers[cuahang].image. Phần tử danh sách có name được gọi theo tên, không theo vị trí. Trường có hơn một giá trị giữa 12 cửa hàng phải thuộc một chiều đã khai báo, và bằng nhau trong mỗi nhóm cùng nhãn của chiều đó:

(string Mau, string Nhan)[] khaiBao =
[
    (@"^ConfigMap/cuahang-cauhinh \.data\.CuaHang__(Ma|Ten)$", "banhang.local/cua-hang"),
    (@"^Deployment/cuahang \.spec\.template\.spec\.containers\[cuahang\]\.resources\.", "banhang.local/co"),
    (@"\]\.image$", "banhang.local/vong"),     // container, initContainer, CronJob: cùng image
];
// ...với mỗi trường d có hơn một giá trị giữa 12 cửa hàng:
string? nhan = nhanBienThe.FirstOrDefault(n => d.EndsWith($" .metadata.labels.{n}"))
    ?? khaiBao.FirstOrDefault(k => Regex.IsMatch(d, k.Mau)).Nhan;
string loai, moTa = MoTa(giaTri, Enumerable.Range(0, ch.Length));
if (nhan is null) loai = "VI PHẠM: không khai báo";
else
{
    // Cùng giá trị nhãn thì phải cùng giá trị trường (cùng cỡ thì cùng limit, cùng vòng thì cùng image).
    string ten = nhan.Split('/')[1];
    var lech = Enumerable.Range(0, ch.Length).GroupBy(i => nhanCua(ch[i], nhan))
        .Where(g => g.Select(i => giaTri[i]).Distinct().Count() > 1).ToArray();
    loai = lech.Length == 0 ? $"theo {ten}"
        : $"VI PHẠM: lệch trong nhóm {string.Join(", ", lech.Select(g => $"{ten}={g.Key}"))}";
    if (lech.Length > 0) moTa = string.Join("; ", lech.Select(g => $"{ten}={g.Key}: {MoTa(giaTri, g)}"));
}

Chạy dotnet run code/SoCuaHang.cs -- render; có vi phạm thì exit code 1. CI gọi nó trong kiem.ps1, chạy từ gốc repo.

SoCuaHang.cs: so 12 bản render, in trường khác nhau, báo vi phạmC# · 113 dòng
#:package YamlDotNet@18.1.0
// So 12 bản render: in đúng những trường khác nhau giữa các cửa hàng,
// và báo vi phạm khi một trường khác nhau ngoài những gì repo đã khai báo.
// dotnet run SoCuaHang.cs -- <thư mục chứa ch01.yaml ... ch12.yaml>
using System.Text.RegularExpressions;
using YamlDotNet.RepresentationModel;

string thuMuc = args.Length > 0 ? args[0] : "render";
string[] nhanBienThe = ["banhang.local/cua-hang", "banhang.local/co", "banhang.local/vong"];
// Trường được phép khác nhau (regex trên "Kind/tên .đường.dẫn"), và nhãn quyết định giá trị của nó.
(string Mau, string Nhan)[] khaiBao =
[
    (@"^ConfigMap/cuahang-cauhinh \.data\.CuaHang__(Ma|Ten)$", "banhang.local/cua-hang"),
    (@"^Deployment/cuahang \.spec\.template\.spec\.containers\[cuahang\]\.resources\.", "banhang.local/co"),
    (@"\]\.image$", "banhang.local/vong"),     // container, initContainer, CronJob: cùng image
];

var ban = Directory.GetFiles(thuMuc, "*.yaml").Order()
    .ToDictionary(f => Path.GetFileNameWithoutExtension(f), DocBanRender);
var ch = ban.Keys.ToArray();
var nhanCua = (string c, string nhan) =>
    ban[c].GetValueOrDefault($"Deployment/cuahang .metadata.labels.{nhan}", "(không có)");
var duongDan = ban.Values.SelectMany(b => b.Keys).Distinct().Order(StringComparer.Ordinal).ToArray();

int giong = 0, theoKhaiBao = 0, viPham = 0;
var dongIn = new List<(string Khoa, string Loai, string Duong, string GiaTri)>();
foreach (var d in duongDan)
{
    var giaTri = ch.Select(c => ban[c].GetValueOrDefault(d, "(không có)")).ToArray();
    if (giaTri.Distinct().Count() == 1) { giong++; continue; }
    string? nhan = nhanBienThe.FirstOrDefault(n => d.EndsWith($" .metadata.labels.{n}"))
        ?? khaiBao.FirstOrDefault(k => Regex.IsMatch(d, k.Mau)).Nhan;
    string loai, moTa = MoTa(giaTri, Enumerable.Range(0, ch.Length));
    if (nhan is null) loai = "VI PHẠM: không khai báo";
    else
    {
        // Cùng giá trị nhãn thì phải cùng giá trị trường (cùng cỡ thì cùng limit, cùng vòng thì cùng image).
        string ten = nhan.Split('/')[1];
        var lech = Enumerable.Range(0, ch.Length).GroupBy(i => nhanCua(ch[i], nhan))
            .Where(g => g.Select(i => giaTri[i]).Distinct().Count() > 1).ToArray();
        loai = lech.Length == 0 ? $"theo {ten}"
            : $"VI PHẠM: lệch trong nhóm {string.Join(", ", lech.Select(g => $"{ten}={g.Key}"))}";
        if (lech.Length > 0) moTa = string.Join("; ", lech.Select(g => $"{ten}={g.Key}: {MoTa(giaTri, g)}"));
    }
    if (loai.StartsWith("VI PHẠM")) viPham++; else theoKhaiBao++;
    int cach = d.IndexOf(' ');
    dongIn.Add((d[(cach + 1)..] + "|" + string.Join("|", giaTri), loai, d, moTa));
}

int soTaiLieu = ban.Values.Max(b => b.Keys.Select(k => k[..k.IndexOf(' ')]).Distinct().Count());
Console.WriteLine($"{ch.Length} bản render, {soTaiLieu} tài liệu mỗi bản, {duongDan.Length} trường lá");
Console.WriteLine($"giống nhau ở cả {ch.Length}: {giong} | khác theo khai báo: {theoKhaiBao} | vi phạm: {viPham}");
// Gộp một trường lặp ở nhiều tài liệu (nhãn gắn lên mọi tài liệu) thành một dòng.
foreach (var nhom in dongIn.GroupBy(x => x.Khoa))
{
    var dau = nhom.First();
    string noi = nhom.Count() == 1 ? dau.Duong
        : $"{nhom.Count()} tài liệu {dau.Duong[(dau.Duong.IndexOf(' ') + 1)..]}";
    Console.WriteLine($"[{dau.Loai}]\n  {noi}\n    {dau.GiaTri}");
}
return viPham == 0 ? 0 : 1;

// Giá trị kèm danh sách cửa hàng, nhóm ít cửa hàng in trước; 12 giá trị khác nhau thì chỉ in đầu và cuối.
string MoTa(string[] giaTri, IEnumerable<int> chon)
{
    if (giaTri.Distinct().Count() == ch.Length)
        return $"{ch.Length} giá trị, mỗi cửa hàng một: {ch[0]}={giaTri[0]} ... {ch[^1]}={giaTri[^1]}";
    // Bỏ phần đầu chung tới dấu "/" cuối: registry.banhang.local/banhang/cuahang:2.1.0 -> cuahang:2.1.0
    var chung = giaTri.Aggregate((a, b) => new string(a.Zip(b).TakeWhile(p => p.First == p.Second).Select(p => p.First).ToArray()));
    int cat = chung.LastIndexOf('/') + 1;
    return string.Join(" | ", chon.GroupBy(i => giaTri[i][cat..]).OrderBy(g => g.Count())
        .Select(g => $"{g.Key}: {string.Join(" ", g.Select(i => ch[i]))}"));
}

// Một file render -> "Kind/tên .đường.dẫn" -> giá trị lá.
static Dictionary<string, string> DocBanRender(string file)
{
    var yaml = new YamlStream();
    using (var r = File.OpenText(file)) yaml.Load(r);
    var ra = new Dictionary<string, string>(StringComparer.Ordinal);
    foreach (var doc in yaml.Documents)
    {
        if (doc.RootNode is not YamlMappingNode goc) continue;     // tài liệu rỗng giữa hai dấu ---
        var meta = (YamlMappingNode)goc["metadata"];
        Phang(goc, $"{goc["kind"]}/{BoHash(meta["name"].ToString())} ", ra);
    }
    return ra;
}

static void Phang(YamlNode node, string duong, Dictionary<string, string> ra)
{
    switch (node)
    {
        case YamlMappingNode m:
            foreach (var (k, v) in m.Children) Phang(v, $"{duong}.{k}", ra);
            break;
        case YamlSequenceNode s:
            for (int i = 0; i < s.Children.Count; i++)
            {
                // Phần tử có "name" (container, cổng, biến môi trường) gọi theo tên, không theo vị trí.
                string khoa = s.Children[i] is YamlMappingNode pt
                    && pt.Children.TryGetValue(new YamlScalarNode("name"), out var ten) ? ten.ToString() : i.ToString();
                Phang(s.Children[i], $"{duong}[{khoa}]", ra);
            }
            break;
        default:
            ra[duong] = duong.EndsWith(".name") ? BoHash(node.ToString()) : node.ToString();
            break;
    }
}

// configMapGenerator thêm hậu tố hash 10 ký tự theo nội dung; nội dung đã được so riêng.
static string BoHash(string ten) => Regex.Replace(ten, "-[2456789bcdfghkmt]{10}$", "");
kiem.ps1: bước CI chạy ở mỗi pull requestPowerShell · 19 dòng
# CI của repo GitOps: render 12 overlay, kiểm schema, so 12 bản render.
# Chạy từ gốc repo: .\kiem.ps1
$ErrorActionPreference = 'Stop'
$k8s = '1.36.5'                       # Kubernetes của k3s v1.36.5+k3s1 (kênh stable)
New-Item -ItemType Directory -Force render, .schema-cache | Out-Null
Remove-Item render\*.yaml -ErrorAction SilentlyContinue
foreach ($ch in Get-ChildItem cuahang -Directory) {
  & .\tools\kustomize.exe build "cuahang/$($ch.Name)" -o "render/$($ch.Name).yaml"
  if ($LASTEXITCODE) { throw "kustomize build $($ch.Name) lỗi" }
}
# -cache: schema Kubernetes tải một lần từ GitHub rồi giữ lại, CI không phụ thuộc mạng mỗi lần chạy.
& .\tools\kubeconform.exe -strict -summary -cache .schema-cache -kubernetes-version $k8s render/
if ($LASTEXITCODE) { throw 'kubeconform: manifest cửa hàng sai schema' }
# Schema của CRD Flux sinh từ flux install --export v2.9.6 bằng openapi2jsonschema.py của kubeconform.
& .\tools\kubeconform.exe -strict -summary `
  -schema-location 'schema-flux/{{ .ResourceKind }}_{{ .ResourceAPIVersion }}.json' clusters/
if ($LASTEXITCODE) { throw 'kubeconform: Flux Kustomization sai schema' }
dotnet run code/SoCuaHang.cs -- render
if ($LASTEXITCODE) { throw 'SoCuaHang: có trường khác nhau ngoài khai báo' }

6. Chứng minh: 0 vi phạm với repo đúng, 3/3 trường bị bắt khi sửa tay ch09

Bài chưa dựng cụm thật: không chạy Docker, k3s hay Flux trên cụm. Bằng chứng: cấu hình đã qua công cụ kiểm, công cụ .NET đã chạy, và mô hình thời gian theo tài liệu Flux. Máy chạy: laptop Intel Core Ultra 5 125U, Windows 11, .NET 10.0.12. Repo mẫu có ba trạng thái, mỗi trạng thái một commit:

Trạng thái Thay đổi trong Git Giống ở cả 12 Khác theo khai báo Vi phạm (exit)
A. 12 cửa hàng trên 2.1.0 Commit đầu 196 28 0 (0)
B. ch03, ch07 lên 2.2.0 1 dòng ở vong/canary 193 31 0 (0)
C. Sửa tay ch09 trên B 12 dòng ở cuahang/ch09 192 29 3 (1)

Mỗi bản render có 8 tài liệu và 224 trường lá. Ở cả ba trạng thái, kubeconform cho 96/96 tài liệu hợp lệ, và 12 file Flux Kustomization hợp lệ với schema CRD sinh từ Flux v2.9.6.

Sửa tay ch09 vẫn hợp lệ với schema, nhưng tạo 3 trường lệch mà bước so render bắt được

Khác theo khai báoVi phạm
A. 12 cửa hàng trên 2.1.028 trường0 trườngB. Canary ch03, ch07 lên 2.2.031 trường0 trườngC. Sửa tay ch0929 trường3 trường
224 trường lá mỗi bản render; phần còn lại giống nhau ở cả 12 cửa hàng. kustomize v5.8.2, kubeconform v0.8.0 với Kubernetes 1.36.5 cho 96/96 tài liệu hợp lệ ở cả ba trạng thái.
Bảng số liệu
Khác theo khai báoVi phạm
A. 12 cửa hàng trên 2.1.028 trường0 trường
B. Canary ch03, ch07 lên 2.2.031 trường0 trường
C. Sửa tay ch0929 trường3 trường

Ở trạng thái C, overlay ch09 có patch đưa image về 2.0.3 và memory limit lên 1Gi, cộng literal DongBo__ChuKyGiay=5: ba thay đổi tay của tuần trước, giờ đi qua Git. Trích output của SoCuaHang:

[VI PHẠM: không khai báo]
  ConfigMap/cuahang-cauhinh .data.DongBo__ChuKyGiay
    5: ch09 | 30: ch01 ch02 ch03 ch04 ch05 ch06 ch07 ch08 ch10 ch11 ch12
[VI PHẠM: lệch trong nhóm vong=on-dinh]
  Deployment/cuahang .spec.template.spec.containers[cuahang].image
    vong=on-dinh: cuahang:2.0.3: ch09 | cuahang:2.1.0: ch01 ch02 ch04 ch05 ch06 ch08 ch10 ch11 ch12
[VI PHẠM: lệch trong nhóm co=vua]
  Deployment/cuahang .spec.template.spec.containers[cuahang].resources.limits.memory
    co=vua: 1Gi: ch09 | 512Mi: ch03 ch04 ch07 ch10

Ba quan sát:

  • Schema không bắt được lệch. kubeconform chỉ bắt lỗi hình dạng: limit: thay cho limits: cho ra "additional properties 'limit' not allowed", prune: yes-please cho ra "got string, want boolean". Memory limit hợp lệ nhưng lệch nhóm thì chỉ bước so render thấy.
  • Canary đúng phạm vi. Commit B đổi 1 dòng trong Git; so render trước và sau, chỉ ch03.yaml và ch07.yaml đổi, mỗi file đúng 3 dòng image: container chính, init container và CronJob. Số trường khác theo khai báo tăng từ 28 lên 31.
  • Hậu quả thấy trước khi merge. Render của ch09 đổi 7 dòng, trong đó 4 dòng là tên ConfigMap mới: hash đổi nên pod template đổi, và Deployment sẽ rollout.

Bản render của CI phải trùng với thứ Flux áp. flux build kustomization --dry-run của Flux CLI v2.9.6 build không cần cụm, theo file Kustomization của từng máy. So bằng SoCuaHang, 12/12 cửa hàng cho 224/224 trường giống bản của kustomize, sau khi bỏ hai nhãn sở hữu mà Flux thêm. Cả chuỗi kiem.ps1 chạy 5,9–12,1 giây ở ba lần đo khi schema đã nằm trong cache. Một lần chạy chưa có -cache dừng ở bước kubeconform, nhiều khả năng vì tải schema lỗi.

Giới hạn thời gian sau là mô hình suy từ tài liệu Flux, không phải số đo. Commit merge xong, máy thấy revision mới trong tối đa một interval của GitRepository, 1 phút theo mặc định của flux bootstrap, rồi áp ngay. Một kubectl edit bị trả lại trong tối đa một interval của Kustomization, 10 phút ở repo này. Thời gian kéo image và rollout cộng thêm.

Điều chưa chứng minh: Flux chạy thật trên k3s v1.36.5+k3s1, thời gian trả lại kubectl edit, hành vi khi máy mất mạng nhiều ngày, và RAM thật hai controller dùng trên máy 8 GB.

7. Kết luận

12 máy hết lệch khi trạng thái mong muốn của từng máy nằm trong Git và một agent trên chính máy đó kéo về, áp và trả lại thay đổi tay. Flux không chặn được thay đổi sai đi vào Git; một bước CI so 12 bản render theo các chiều đã khai báo làm việc đó.

Trong dự án .NET của bạn:

  • Tách cấu hình theo chiều: overlay trung gian cho vòng phát hành, Component cho cỡ, overlay lá cho danh tính. Dùng labels, không dùng commonLabels.
  • Ghi mã và tên cửa hàng bằng configMapGenerator với behavior: merge, đọc bằng IConfiguration["CuaHang:Ma"]; hash của ConfigMap tự kích rollout khi cấu hình đổi.
  • Mỗi pull request chạy kustomize build cho mọi overlay, kubeconform -strict -kubernetes-version đúng bản của k3s, rồi công cụ so render viết bằng YamlDotNet.
  • Bootstrap Flux với --components=source-controller,kustomize-controller, đặt prune: true và wait: true; đổi tag ở vong/canary trước, vong/on-dinh sau.

Những chỗ hay hiểu sai

  • "Qua kubeconform là cấu hình đúng." Schema chỉ kiểm hình dạng; ba thay đổi tay ở ch09 đều hợp lệ.
  • "Có GitOps thì không cần CI." Flux áp trung thành mọi thứ trong Git, kể cả commit sửa tay.

Đọc tiếp

Nguồn

Đọc tiếp

Trong Docker Swarm và k3s

Swarm hay k3s cho trụ sở: ánh xạ stack BanHang.Api và chuyển không gián đoạn

kompose dịch stack BanHang.Api thành manifest qua kubeconform, nhưng app không khởi động và mọi thiết lập an toàn của ba bài Swarm rơi mất. Bảng tiêu chí có nguồn, ánh xạ 43 khóa của stack có công cụ C# kiểm, và một kế hoạch chuyển từng máy mà hai cụm cùng phục vụ sau load balancer.

13 phút đọc

Trong Docker Swarm và k3s

Máy chủ cửa hàng chạy k3s: backend tự thay khi treo trên mini PC 8 GB

Backend ở cửa hàng ch04 treo lúc 19:40 và cửa hàng không bán được 52 phút, vì systemd chỉ chạy lại tiến trình đã thoát. k3s gói Kubernetes vào một binary dưới 100 MB với SQLite, containerd, Flannel, Traefik, ServiceLB và local-path; Deployment có liveness probe thay pod treo trong 32–67 giây theo mô hình.

14 phút đọc

Trong Docker Swarm và k3s

Probe, giới hạn bộ nhớ và HPA trên k3s: pod cửa hàng trụ được flash sale

Flash sale 9/9 ở ch01, pod BanHang.CuaHang restart liên tục vì liveness gọi endpoint có kiểm trụ sở, heap limit ghi bằng số thập phân thành 16 GiB, và một pod không gánh nổi tải. Tách ba probe, để GC tự lấy 75% memory limit, và chạy HPA 2–3 pod; kiểm bằng app .NET chạy thật, Job Object 512 MiB và mô hình kubelet, HPA.

13 phút đọc

Trong Docker Swarm và k3s

Dữ liệu offline của cửa hàng trên k3s: PVC local-path, tên node và sao lưu bằng CronJob

SSD của máy ch06 hỏng sau gần 3 giờ mất mạng và 41 đơn bán offline mất theo; ở ch02, đổi tên máy làm pod kẹt Pending vì PV local-path gắn với tên node cũ. Cố định tên node, sao lưu SQLite mỗi 15 phút bằng API backup qua CronJob ra ổ thứ hai, và để initContainer tự khôi phục volume trống.

13 phút đọc