Deploy bằng sed sửa vài dòng manifest rồi kubectl apply chạy được với một site. Nhưng một website Laravel trên Kubernetes cần tới gần chục object: Deployment, Service, Ingress, Certificate, ConfigMap, Secret, secret kéo image, HPA. Nhân với hàng chục site, copy manifest rồi sửa tay chắc chắn sẽ lệch nhau. Đây là lúc cần Helm chart. Tài liệu này giải thích Helm chart là gì, cấu trúc một chart cho website Laravel, cách tổ chức values cho từng site, cách khai NFS ngay trong Deployment, khái niệm release, revision, rollback, và so sánh với manifest rời.

1. Helm chart là gì

Helm là trình quản lý gói cho Kubernetes, giống apt với Debian hay composer với PHP. Đơn vị đóng gói của Helm gọi là chart. Một Helm chart gồm hai phần:

  • templates: các file manifest Kubernetes có chỗ trống, viết bằng cú pháp template của Go ({{ .Values.domain }}).
  • values: các giá trị điền vào chỗ trống, đặt trong values.yaml mặc định và ghi đè bằng file riêng (-f) hoặc tham số --set.

Ba khái niệm còn lại:

  • release: một lần cài Helm chart vào cluster, có tên riêng. Cùng một chart, mỗi website là một release.
  • revision: mỗi lần helm upgrade tạo một revision mới của release, đánh số 1, 2, 3...
  • rollback: đưa release về manifest và values của một revision cũ.

Với Helm 3, thông tin từng revision được lưu thành Secret ngay trong namespace của release (tên dạng sh.helm.release.v1.<release>.v<số>), không cần thành phần nào chạy trong cluster. Tài liệu gốc về cấu trúc Helm chart ở trang Charts của Helm.

2. Khởi tạo Helm chart bằng helm create

Cách nhanh nhất là sinh khung Helm chart bằng helm create web rồi chỉnh lại. Chart sinh ra mang theo nhiều giá trị mặc định cần sửa ngay, dễ thấy nhất là Chart.yaml:

apiVersion: v2
name: web
description: A Helm chart for Kubernetes
type: application
version: 0.1.0         # phiên bản của chart, tăng mỗi lần đổi template
appVersion: "1.16.0"   # giá trị mẫu, nên sửa hoặc đừng dùng làm tag image

Quy trình hợp lý khi chuyển sang Helm chart: viết template, chạy helm template với values của một site thử, sửa cho tới khi manifest sinh ra giống bộ manifest rời đang chạy, rồi mới cài thử lên cluster.

3. Cấu trúc Helm chart cho website Laravel

web/
├── Chart.yaml
├── values.yaml              # giá trị mặc định
├── example/
│   └── site-values.yaml     # values mẫu có placeholder cho pipeline
└── templates/
    ├── _helpers.tpl         # các hàm đặt tên, label dùng chung
    ├── deployment.yaml      # Laravel + initContainer migrate
    ├── service.yaml
    ├── ingress.yaml         # ingress-nginx, TLS
    ├── certificate.yaml     # cert-manager, ClusterIssuer Let's Encrypt
    ├── configmap.yaml       # biến môi trường không bí mật
    ├── secret.yaml          # APP_KEY, mật khẩu DB...
    ├── regcred.yaml         # secret kéo image từ registry
    ├── hpa.yaml
    ├── serviceaccount.yaml
    ├── NOTES.txt
    └── tests/test-connection.yaml

Mỗi file trong templates/ của Helm chart tương ứng một object từng viết tay. ConfigMap và Secret giữ cách tổ chức ở bài ConfigMap và Secret: biến môi trường Laravel trên Kubernetes, Ingress và Certificate giữ cách làm ở bài ingress-nginx và cert-manager: HTTPS tự động trên Kubernetes. Khác biệt là mọi tên, domain, mật khẩu giờ lấy từ values.

4. _helpers.tpl: đặt tên và label một chỗ

_helpers.tpl chứa các template có tên (named template), gọi bằng include. Hai hàm quan trọng nhất của Helm chart:

{{- define "web.fullname" -}}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}

{{- define "web.selectorLabels" -}}
app.kubernetes.io/name: {{ include "web.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
  • fullname: tên gốc cho mọi object của release. Cắt ở 63 ký tự vì tên nhiều loại object phải là DNS label hợp lệ.
  • selectorLabels: bộ label để Deployment chọn pod và Service tìm pod. Hai bên dùng chung một hàm nên không thể lệch nhau.
  • Có thể thêm hàm imagePullSecret ghép username/password trong values thành chuỗi .dockerconfigjson mã hoá base64 cho Secret kéo image.

Cách viết named template được hướng dẫn ở mục named templates trong Chart Template Guide.

5. Values: một file mẫu, mỗi site một bản

values.yaml mặc định giữ giá trị chung: image, resources (theo cách chọn ở bài Requests và limits, QoS và nodeAffinity: xếp pod lên đúng node), annotation của Ingress. Mỗi site có một file values riêng, sinh từ file mẫu chứa placeholder để pipeline thay (lược bớt, bí mật thay bằng <...>):

domain: "[DOMAIN]"
appName: "[APP_NAME]"
dbHost: "[DB_HOST]"
dbDatabase: "[DB_DATABASE]"
image:
  repository: [IMAGE_NAME]
  tag: "[IMAGE_TAG]"
autoscaling:
  enabled: true
  minReplicas: 1
  targetCPUUtilizationPercentage: 80
volumes:
  nfsIp: [NFS_IP]
  nfsSharedFolder: [NFS_SHARED_FOLDER]
ingress:
  enabled: true
  className: nginx
  annotations:
    nginx.ingress.kubernetes.io/proxy-body-size: "128m"
    nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"

Nên lưu file values của từng site vào Git, theo thư mục cluster và tên site, và để pipeline tự commit sau mỗi lần deploy. Khi đó muốn biết một site đang chạy với cấu hình gì chỉ cần mở đúng một file.

Mặt trái: values của từng site chứa mật khẩu database, APP_KEY và thông tin đăng nhập registry. Repo chứa values phải là repo riêng tư với quyền truy cập hẹp, và nên mã hoá phần bí mật (ví dụ helm-secrets với SOPS) hoặc tách bí mật ra công cụ quản lý riêng ngay từ đầu.

6. NFS khai inline trong Deployment

Website Laravel thường cần lưu file người dùng tải lên và theme đã chỉnh ra ngoài container. Cách đơn giản nhất là khai volume NFS thẳng trong pod template, không qua PersistentVolume hay PersistentVolumeClaim:

volumes:
- name: {{ include "web.fullname" . }}-volume
  nfs:
    server: {{ .Values.volumes.nfsIp }}
    path: {{ .Values.volumes.nfsSharedFolder }}/{{ include "web.fullname" . }}
    readOnly: false

Container mount các thư mục con bằng subPath, ví dụ public, themes, plugins và storage/framework/sessions. Mỗi site có một thư mục riêng trên máy NFS, tên theo fullname của release.

ĐượcMất
Đơn giản: không phải tạo PV, PVC cho từng siteVolume inline không có chỗ khai mountOptions (phiên bản NFS, timeout...), chỉ PersistentVolume mới có
Xoá release không đụng tới dữ liệu trên NFSThư mục trên NFS phải tồn tại trước, pipeline phải tạo nó
Đường dẫn dễ đoán, dễ backup theo siteKubernetes không biết gì về dung lượng hay vòng đời của volume; phân quyền dựa vào cấu hình export của máy NFS

So với các loại volume ở bài Volume, PersistentVolume, PVC và StorageClass trên Kubernetes, đây là lựa chọn đơn giản nhất, đủ cho giai đoạn đầu khi số site còn ít.

7. Release, revision và rollback trong thực tế

Pipeline cài Helm chart cho từng site (ở đây tên release trùng namespace, mỗi site một namespace):

# render ra manifest để kiểm tra, không đụng cluster
helm template $RELEASE ./web -f $VALUES_FILE --debug

# chạy thử phía server
helm upgrade --install $RELEASE ./web -f $VALUES_FILE \
  -n $RELEASE --create-namespace --dry-run

# cài mới hoặc nâng cấp
helm upgrade --install $RELEASE ./web -f $VALUES_FILE \
  -n $RELEASE --create-namespace

Các lệnh hằng ngày với release:

helm list -A                         # mọi release trên cluster
helm history <release> -n <ns>       # các revision, thời gian, trạng thái
helm get values <release> -n <ns>    # values đang dùng
helm rollback <release> 3 -n <ns>    # quay về revision 3
helm uninstall <release> -n <ns>

Bẫy cần biết khi dùng Helm chart: nếu image dùng tag cố định với pullPolicy: Always và code mới được kéo về nhờ đổi một label trong pod template, thì helm rollback chỉ đưa label về giá trị cũ; pod được tạo lại nhưng vẫn kéo bản mới nhất của cùng tag. Rollback của Helm khi đó chỉ khôi phục cấu hình, không khôi phục code. Dữ liệu trên NFS và database cũng không quay lui theo. Muốn rollback được code, tag phải gắn với phiên bản code. Chi tiết lệnh ở trang helm rollback.

8. Manifest rời hay Helm chart

Tiêu chíManifest rời + sedHelm chart
Thêm một siteCopy cả thư mục, sửa tên ở nhiều fileMột file values, một lệnh helm upgrade --install
Sửa cấu hình chungSửa từng bản copySửa template một lần, upgrade lần lượt các release
Biết đã deploy gì, khi nàoChỉ có git log của filehelm history theo từng release
Gỡ siteNhớ xoá đủ từng objecthelm uninstall xoá mọi object của release
Độ khó khi đọcYAML thuần, ai cũng đọc đượcPhải đọc template Go, khoảng trắng và nindent dễ sai
DebugNhìn thẳng filePhải helm template ra mới thấy manifest thật

9. Lỗi thường gặp khi viết Helm chart

  • Namespace viết cứng trong template. Đặt namespace: {{ fullname }} trong từng file dễ lệch với namespace truyền bằng -n. Bỏ hẳn trường namespace khỏi template, để Helm gán theo -n.
  • HPA không tìm thấy Deployment. scaleTargetRef.name sinh từ helm create là {{ fullname }}; nếu bạn đổi tên Deployment thành {{ fullname }}-deployment, HPA trỏ vào khoảng trống. Đổi tên một object thì dò mọi chỗ tham chiếu tới nó, kể cả file test.
  • Đổi selector khi đã có release. Selector của Deployment không sửa được sau khi tạo; upgrade sẽ báo field is immutable. Chốt selector trước khi có site thật.
  • Lỗi nằm im trong nhánh điều kiện. Một khối {{- with .Values.podSecurityContext }} gọi toYaml thiếu tham số sẽ không bao giờ lộ lỗi nếu giá trị đang rỗng. Chạy helm template với vài bộ values khác nhau để phủ các nhánh.
  • Tag image dự phòng theo appVersion. Template mặc định viết .Values.image.tag | default .Chart.AppVersion. Nếu values quên tag, Helm dùng 1.16.0, một tag không tồn tại, và pod kẹt ở ImagePullBackOff.

10. Khuyến nghị

  • Helm chart đáng giá từ site thứ hai trở đi. Với một site, manifest rời vẫn dễ hiểu hơn.
  • Helm chart sinh từ helm create mang theo nhiều giá trị mặc định: đọc lại từng dòng, đặc biệt là tên object và các tham chiếu chéo.
  • Luôn helm template --debug và --dry-run trước khi cài thật; hai bước này rẻ và bắt được phần lớn lỗi cú pháp.
  • Rollback của Helm không thay được chiến lược đánh tag image.
  • Values của Helm chart là nơi tập trung bí mật: quyết định sớm cách bảo vệ nó.

Bài tiếp theo

Cầu nối Bitbucket và GitLab CI bằng API và polling. Code nằm trên Bitbucket, pipeline build và deploy chạy trên GitLab: cách hai bên nói chuyện với nhau bằng một lệnh curl và một vòng lặp chờ kết quả.