Một instance MongoDB không xác thực có thể bị xoá sạch dữ liệu, và khi đưa website Laravel dùng MongoDB lên Kubernetes thường gặp thêm hai lỗi nối tiếp nhau. Lỗi đầu là The SCRAM_SHA_1 authentication mechanism requires libmongoc built with ENABLE_SSL ngay khi ứng dụng kết nối. Sửa xong lỗi đó thì tới Authentication failed, dù tài khoản và mật khẩu đúng. Sau khi chạy được, câu hỏi tiếp theo là nạp dữ liệu cũ vào pod và sao lưu định kỳ ra ngoài cluster. Tài liệu này đi theo thứ tự: vì sao phải bật xác thực, triển khai MongoDB bằng StatefulSet, hai lỗi SCRAM và authSource, rồi restore, backup bằng kubectl cp và một lỗi vặt về namespace. Phần nào chưa kiểm chứng được sẽ nói rõ là giả thuyết.

Cách tách cấu hình khỏi image nằm ở ConfigMap và Secret: biến môi trường Laravel trên Kubernetes, cách đóng gói chart dùng lại ở Chart dùng chung: tái sử dụng Helm cho Laravel, MySQL và Redis.

Vì sao phải bật authorization ngay từ đầu

MongoDB mặc định không bắt đăng nhập. Một instance không xác thực mà mở ra mạng sẽ cho ai kết nối cũng đọc, ghi và xoá được. Một kiểu sự cố phổ biến: log của mongod có dòng dropDatabase xoá sạch database ứng dụng, rồi xuất hiện một database mới tên dạng READ_ME_TO_RECOVER_YOUR_DATA chứa lời đòi tiền chuộc. Log không chứng minh được ai làm hay đi vào bằng cách nào, nhưng hãy coi "MongoDB không xác thực" là lỗ hổng. Cách xử lý là khôi phục từ backup và bật xác thực.

Với image mongo chính thức trên Kubernetes, chỉ cần đặt MONGO_INITDB_ROOT_* là xác thực được bật. Với một instance tự cài đặt, thứ tự bật xác thực là:

  1. Kết nối cục bộ, tạo user quản trị trong database admin, ví dụ role userAdminAnyDatabase.
  2. Thêm vào mongod.conf: security.authorization: enabled, rồi khởi động lại dịch vụ.
  3. Đăng nhập bằng user quản trị (--authenticationDatabase admin), tạo user riêng cho từng database ứng dụng với role readWrite.
  4. Cấu hình ứng dụng dùng user riêng đó.

Về bindIp: đừng đổi chung một lượt với bật xác thực, vì dễ làm ứng dụng mất kết nối. Hãy bật xác thực trước, kiểm tra ứng dụng ổn, rồi mới tính chuyện mạng. Trên Kubernetes, không mở Service của MongoDB ra ngoài cluster: dùng ClusterIP hoặc headless Service, và cân nhắc NetworkPolicy, vì container mặc định lắng nghe trên mọi interface.

Triển khai MongoDB trên Kubernetes

Với MongoDB, dùng StatefulSet có PVC riêng. Image mongo chính thức tạo user quản trị lần đầu từ hai biến MONGO_INITDB_ROOT_USERNAME và MONGO_INITDB_ROOT_PASSWORD. Lưu ý quan trọng: hai biến này chỉ có tác dụng khi thư mục dữ liệu còn trống. Đổi chúng sau khi PVC đã có dữ liệu sẽ không đổi mật khẩu. Tạo Secret trước khi áp manifest:

kubectl create secret generic mongo-auth \
  --from-literal=username=<ten-user> \
  --from-literal=password=<mat-khau> -n db-1
apiVersion: v1
kind: Service
metadata:
  name: mongo
spec:
  clusterIP: None          # headless: truy cập theo tên DNS của pod
  selector:
    app: mongo
  ports:
    - port: 27017
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: mongo
spec:
  serviceName: mongo
  replicas: 1
  selector:
    matchLabels:
      app: mongo
  template:
    metadata:
      labels:
        app: mongo
    spec:
      containers:
        - name: mongo
          image: mongo:8
          ports:
            - containerPort: 27017
          env:
            - name: MONGO_INITDB_ROOT_USERNAME
              valueFrom:
                secretKeyRef: { name: mongo-auth, key: username }
            - name: MONGO_INITDB_ROOT_PASSWORD
              valueFrom:
                secretKeyRef: { name: mongo-auth, key: password }
          volumeMounts:
            - name: data
              mountPath: /data/db
  volumeClaimTemplates:
    - metadata:
        name: data
      spec:
        accessModes: ["ReadWriteOnce"]
        resources:
          requests:
            storage: 20Gi

Hãy để ý containerPort: một lỗi gõ hay gặp là 27107 thay vì 27017. Trường này chỉ mang tính mô tả nên pod vẫn chạy, nhưng gây nhầm. Ứng dụng kết nối headless Service bằng tên mongo.<namespace>.svc.cluster.local, không qua IP. Bài Volume, PersistentVolume, PVC và StorageClass giải thích phần lưu trữ.

Lỗi 1: SCRAM_SHA_1 requires libmongoc built with ENABLE_SSL

Thông báo này đến từ extension PHP mongodb, không phải từ server. Extension được biên dịch bằng pecl trong lúc build image; nếu lúc đó chưa có các gói phát triển OpenSSL thì libmongoc bên trong được build không có SSL, và không dùng được cơ chế SCRAM. Cách sửa là cài gói trước pecl:

RUN apt-get update && apt-get install -y \
      libssl-dev libcurl4-openssl-dev pkg-config \
 && pecl install mongodb \
 && docker-php-ext-enable mongodb

Thứ tự quyết định: cài libssl-dev sau pecl install không có tác dụng. Phần đóng gói image Laravel nằm ở Docker hoá Laravel.

Lỗi 2: Authentication failed và authSource

Khi lỗi SSL hết, lỗi xác thực lộ ra. Nguyên nhân thường là authSource, tức database mà MongoDB dùng để tra user. User tạo bằng hai biến MONGO_INITDB_ROOT_* nằm trong database admin và có role root. Bạn kiểm chứng bằng:

mongosh -u root-user --authenticationDatabase admin
use admin
db.getUser('root-user')
// roles: [ { role: 'root', db: 'admin' } ]

Một số hướng dẫn trên mạng ghi role của user init là userAdminAnyDatabase. Đó là role bạn tự chọn khi tạo user quản trị thủ công; image mongo cấp root, như kết quả db.getUser ở trên cho thấy.

Vậy nếu cấu hình Laravel đặt authSource bằng tên database ứng dụng, MongoDB sẽ tìm user trong database ứng dụng, không thấy, và trả Authentication failed. Với user init, authSource phải là admin. Ngược lại, user riêng tạo bên trong database ứng dụng (kiểu mỗi database một user readWrite) thì authSource là chính database đó. Để một cấu hình chạy được cả hai môi trường, tách thành biến:

'mongodb' => [
    'driver'   => 'mongodb',
    'host'     => env('MONGO_DB_HOST', '127.0.0.1'),
    'port'     => env('MONGO_DB_PORT', 27017),
    'database' => env('MONGO_DB_DATABASE', 'app_db'),
    'username' => env('MONGO_DB_USERNAME'),
    'password' => env('MONGO_DB_PASSWORD'),
    'options'  => [
        'authSource' => env('MONGO_DB_AUTH_DATABASE', 'admin'),
    ],
],

Một lỗi hay gặp khi chép cấu hình là thiếu dấu phẩy giữa các khoá, gây lỗi cú pháp PHP. Trên K8s với một pod MongoDB chỉ chứa một database, dùng user root cho ứng dụng chạy được, nhưng nguyên tắc ít quyền vẫn khuyên một user readWrite riêng.

Restore MongoDB bằng kubectl cp và mongorestore

Quy trình nạp một thư mục dump vào pod MongoDB: sao chép vào /tmp của pod, rồi chạy mongorestore bên trong. kubectl cp cần có tar trong image; image mongo có sẵn.

kubectl cp ./dump mongo-0:/tmp/ -n db-1

kubectl exec -it mongo-0 -n db-1 -- bash -c \
  'mongorestore --username "$MONGO_INITDB_ROOT_USERNAME" \
                --password "$MONGO_INITDB_ROOT_PASSWORD" \
                --authenticationDatabase admin \
                --nsInclude "app_db.*" /tmp/dump'

Dấu nháy đơn khiến biến được thay thế bên trong pod, nên mật khẩu không nằm ở máy, lịch sử shell hay log CI, nhưng vẫn thấy được bằng ps trong pod; nếu cần chặt hơn, dùng --config với tệp YAML mount từ Secret. Cờ --db đã deprecated, hãy dùng --nsInclude. Nếu không đặt --authenticationDatabase, mặc định là admin (hoặc database của --db), nên hãy đặt tường minh admin cho user init.

Có hai hiện tượng khó hiểu khi restore, và nguyên nhân chưa được xác định. Chạy mongorestore --db không kèm xác thực thì báo Unauthorized ... listCollections, điều này hợp lý. Chạy có --nsInclude và tài khoản nhưng kết quả là 0 document(s) restored mà không có lỗi, trong khi dùng đường dẫn tuyệt đối tới thư mục dump của database kèm --db thì thành công.

Giả thuyết có khả năng nhất là đường dẫn dump sai cấp. --nsInclude khớp theo cấu trúc <thư mục dump>/<db>/<collection>.bson; nếu đối số cuối trỏ vào thư mục cha hoặc con sai cấp (ví dụ . khi đang đứng ở /tmp), công cụ khớp 0 collection và báo 0 document mà không lỗi. Đây vẫn chỉ là giả thuyết. Cách kiểm: ls -R /tmp/dump, thêm -v, và chạy với đường dẫn tuyệt đối. Hãy đối chiếu tài liệu mongorestore trước khi chốt.

Backup MongoDB bằng mongodump

Có hai cách sao lưu MongoDB. Cách thứ nhất ghi ra thư mục trong pod rồi copy về:

kubectl exec mongo-0 -n db-1 -- bash -c \
  'mongodump --username "$MONGO_INITDB_ROOT_USERNAME" \
             --password "$MONGO_INITDB_ROOT_PASSWORD" \
             --authenticationDatabase admin \
             --db app_db --out /tmp/dump'

kubectl cp db-1/mongo-0:/tmp/dump ./dump

Cách thứ hai dùng archive qua luồng chuẩn, không cần kubectl cp và không để lại tệp tạm trong pod, theo tài liệu của MongoDB:

kubectl exec mongo-0 -n db-1 -- bash -c \
  'mongodump --username "$MONGO_INITDB_ROOT_USERNAME" \
             --password "$MONGO_INITDB_ROOT_PASSWORD" \
             --authenticationDatabase admin \
             --db app_db --archive --gzip' > app_db.archive.gz

Sau đó nén và đẩy lên object storage riêng tư. Trong CI, lấy thông tin kết nối từ biến được che hoặc Secret, không echo ra log.

Một điều quan trọng: bản backup chỉ có giá trị khi restore được. Một cách kiểm tra sai thường gặp là restore lại chính thư mục đã có sẵn, việc này không chứng minh gì về mongodump. Vòng kiểm đúng xuất phát từ thư mục do mongodump sinh ra, rồi restore sang một tên database khác để so sánh, không đè lên dữ liệu thật:

kubectl exec mongo-0 -n db-1 -- bash -c \
  'mongorestore --username "$MONGO_INITDB_ROOT_USERNAME" \
                --password "$MONGO_INITDB_ROOT_PASSWORD" \
                --authenticationDatabase admin \
                --nsFrom "app_db.*" --nsTo "app_db_check.*" /tmp/dump'

# restore từ archive
kubectl exec -i mongo-0 -n db-1 -- bash -c \
  'mongorestore --username "$MONGO_INITDB_ROOT_USERNAME" \
                --password "$MONGO_INITDB_ROOT_PASSWORD" \
                --authenticationDatabase admin \
                --archive --gzip' < app_db.archive.gz

Vòng round-trip này chưa được thử ở đây, nên hãy tự chạy trên môi trường test trước khi tin vào pipeline backup.

Lỗi vặt: ConfigMap và Secret trùng tên ở hai namespace

Triệu chứng: ConfigMap đã đặt MONGO_DB_USERNAME một giá trị, nhưng trong pod echo ra giá trị cũ. Cần nhớ: configMapKeyRef và secretKeyRef luôn được phân giải trong namespace của chính pod, không nhìn sang namespace khác. Vì vậy một nguyên nhân hay gặp là pod đang chạy ở một namespace có ConfigMap/Secret trùng tên với giá trị cũ, thường do chạy kubectl apply không có -n và manifest không ghi metadata.namespace, nên Deployment rơi vào namespace hiện tại của context (thường là default). Có ba hướng kiểm tra:

  1. Pod đã được khởi động lại sau khi sửa ConfigMap chưa.
  2. kubectl get deploy,cm,secret -A | grep <ten> để tìm bản trùng ở namespace khác.
  3. Biến có bị đặt trùng khoá giữa env và envFrom không.

Việc Deployment bị apply nhầm namespace là suy luận từ cơ chế K8s, hãy xác nhận bằng kubectl get deploy -A. Sửa ConfigMap không làm pod đang chạy đổi biến môi trường; cần khởi động lại pod.

Checklist

  • Bật authorization cho MongoDB, user quản trị ở admin, mỗi ứng dụng một user readWrite.
  • Image PHP cài libssl-dev, libcurl4-openssl-dev, pkg-config trước pecl install mongodb.
  • authSource khớp nơi user được tạo, đặt qua biến môi trường.
  • Dùng --authenticationDatabase và --nsInclude với công cụ dòng lệnh; mật khẩu lấy từ biến trong pod.
  • Restore thử sang tên database khác để xác nhận backup dùng được.
  • Luôn ghi -n hoặc metadata.namespace; kiểm tra ConfigMap, Secret không trùng tên ở namespace thừa.

Tài liệu chính thức: mongorestore và kubectl cp.

Bài tiếp theo

Chứng chỉ wildcard cho hàng chục site trên Kubernetes: Secret TLS và DNS-01. Khi số site tăng, giới hạn phát hành chứng chỉ sẽ là chỗ nghẽn tiếp theo.