❤️ AZDIGI chính thức cập nhật hệ thống blog mới hoàn chỉnh. Tuy nhiên có thể một số bài viết bị sai lệch hình ảnh, hãy ấn nút Báo cáo bài viết ở cuối bài để AZDIGI cập nhật trong thời gian nhanh nhất. Chân thành cám ơn.

Ở phần 7, mình đã tách ứng dụng full-stack thành các thành phần quen thuộc: frontend, backend API, ingress, service và bộ values riêng cho từng môi trường. Đến phần này, chart mới bắt đầu giống một bài toán production hơn, vì backend không thể sống chỉ với biến môi trường giả lập. Nó cần PostgreSQL để lưu dữ liệu lâu dài và Redis để làm cache, session hoặc hàng đợi nhẹ.

Nếu bạn chưa đọc phần trước, có thể xem lại tại NodeJS Full-Stack Part 1: Kiến trúc chart cho React/Next.js + Express (Phần 7/12).

ℹ️ Bài này dùng ví dụ theo kiểu chart tổng hợp (umbrella chart): một chart chính kéo frontend, backend, PostgreSQL và Redis bằng dependencies. Nếu bạn đang dùng external database từ đầu thì vẫn áp dụng được phần kết nối, secret và health check.

Dependencies hay managed service?

Helm dependency rất hợp cho giai đoạn lab, staging nhỏ hoặc những hệ thống bạn muốn dựng nhanh toàn bộ stack chỉ bằng một lệnh helm install. Cách này tiện ở chỗ mọi thứ nằm chung một chart: version được khóa tương đối rõ, values tập trung, rollback cùng lúc, mới vào dự án nhìn cũng dễ hơn.

Nhưng production thì không phải lúc nào cũng nên nhét luôn database vào chart ứng dụng. PostgreSQL và Redis là stateful service. Nâng cấp, backup, restore, mở rộng dung lượng, replication hay monitoring của chúng thường có vòng đời khác với backend. Nếu mỗi lần deploy app lại chạm vào database chart, bạn đang tăng rủi ro nhiều hơn mức cần thiết.

  • Dùng dependency khi cần môi trường khép kín, dựng nhanh, demo, lab, review app, CI preview hoặc hệ thống nhỏ.
  • Dùng external managed service khi cần độ ổn định cao hơn, backup chuẩn, replication, failover, nâng cấp độc lập và quản lý dữ liệu chuyên biệt.

Hiểu nhanh thì dependency giúp bạn có một package hoàn chỉnh. Managed service thì tách phần dữ liệu ra khỏi vòng đời deploy của ứng dụng. Nhiều team đi theo lộ trình rất thực tế: dev và staging dùng dependency để tối ưu tốc độ dựng môi trường, còn production trỏ sang PostgreSQL/Redis bên ngoài.

Kiến trúc full-stack NodeJS trên Kubernetes với PostgreSQL và Redis

Thêm PostgreSQL vào chart

Phổ biến nhất là dùng chart PostgreSQL của Bitnami làm dependency. Bạn chỉ cần khai báo repository, version và condition trong Chart.yaml. Condition cho phép bật tắt bằng values, rất hữu ích khi production chuyển sang external database.

apiVersion: v2
name: fullstack-app
version: 0.1.0
appVersion: "1.0.0"
dependencies:
  - name: postgresql
    version: 15.5.23
    repository: https://charts.bitnami.com/bitnami
    condition: postgresql.enabled
  - name: redis
    version: 20.11.3
    repository: https://charts.bitnami.com/bitnami
    condition: redis.enabled

Sau đó, trong values.yaml, bạn khai báo phần cần thiết cho PostgreSQL. Đừng ôm hết mọi option của chart con nếu bạn chưa dùng tới. Chỉ giữ những biến thực sự liên quan tới app của mình, như auth, database name, persistence và service port.

postgresql:
  enabled: true
  auth:
    username: appuser
    password: changeme-postgres
    database: appdb
  primary:
    persistence:
      enabled: true
      size: 8Gi
    service:
      ports:
        postgresql: 5432

Với cách cài này, service DNS mặc định trong cluster thường sẽ là <release-name>-postgresql. Tên chính xác còn phụ thuộc release name của bạn, nên tốt nhất là đừng hardcode bừa trong code. Nên sinh connection string từ values hoặc từ helper template của Helm để tránh sai tên service khi đổi release.

backend:
  env:
    DB_HOST: "{{ .Release.Name }}-postgresql"
    DB_PORT: "5432"
    DB_NAME: "appdb"
    DB_USER: "appuser"

Ở đây có hai hướng. Một là truyền từng biến riêng như DB_HOST, DB_PORT, DB_NAME. Hai là truyền luôn DATABASE_URL. Nếu backend của bạn dùng Prisma, Sequelize, TypeORM hay Knex, kiểu URL thường gọn hơn vì cả ứng dụng và tool migration đều đọc cùng một định dạng.

backend:
  extraEnv:
    - name: DATABASE_URL
      valueFrom:
        secretKeyRef:
          name: fullstack-app-db
          key: database-url
Dependencies PostgreSQL và Redis trong Chart.yaml

⚠️ Nếu bạn để password trực tiếp trong values rồi commit lên Git, coi như secret đã đi dạo khắp nơi. Dev thì còn tạm chấp nhận cho lab local, staging và production thì nên chuyển sang Secret riêng, External Secrets hoặc CI/CD inject lúc deploy.

Thêm Redis vào chart

thêm Redis vào chart

Redis thường được thêm vào full-stack chart cho ba việc: cache dữ liệu đọc nhiều, lưu session tạm và làm message broker nhẹ cho job queue. Không phải app nào cũng cần cả ba, nhưng phần lớn backend hiện đại sẽ cần ít nhất cache hoặc session store.

redis:
  enabled: true
  architecture: standalone
  auth:
    enabled: true
    password: changeme-redis
  master:
    persistence:
      enabled: true
      size: 2Gi

Với session hoặc cache, bạn thường chỉ cần endpoint Redis, password và DB index. Nhiều thư viện Node.js như ioredis, redis, connect-redis đều hỗ trợ kiểu URL rất tiện: redis://:password@host:6379/0.

backend:
  extraEnv:
    - name: REDIS_URL
      valueFrom:
        secretKeyRef:
          name: fullstack-app-redis
          key: redis-url
    - name: SESSION_STORE
      value: redis
    - name: CACHE_DRIVER
      value: redis

Nếu app đang chạy nhiều replica backend, session lưu trong memory gần như không còn phù hợp. Request đầu vào có thể bị load balancer đẩy sang pod khác và người dùng sẽ thấy mất phiên đăng nhập. Đó là lúc Redis session store phát huy tác dụng, vì mọi pod cùng đọc ghi từ một nơi.

Luồng kết nối từ frontend đến backend, PostgreSQL và Redis

Backend cần sửa gì để kết nối DB và Redis?

backend cần sửa gì để kết nối DB và Redis?

Helm chỉ giải quyết chuyện cấp phát tài nguyên và truyền cấu hình. Backend vẫn phải tự kết nối, retry khi dependency chưa sẵn sàng và public health endpoint cho Kubernetes kiểm tra. Nếu ứng dụng thiếu các lớp này thì chart đẹp mấy cũng chưa đủ.

Một ví dụ backend Express dùng PostgreSQL với pg và Redis với ioredis có thể viết gọn như sau:

const express = require('express');
const { Pool } = require('pg');
const Redis = require('ioredis');
const app = express();
const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
  max: 20,
  idleTimeoutMillis: 30000,
});
const redis = new Redis(process.env.REDIS_URL, {
  maxRetriesPerRequest: 1,
  enableReadyCheck: true,
});
app.get('/healthz', async (req, res) => {
  try {
    await pool.query('SELECT 1');
    await redis.ping();
    res.status(200).json({ status: 'ok' });
  } catch (error) {
    res.status(503).json({ status: 'degraded', error: error.message });
  }
});
app.listen(3000, () => {
  console.log('API listening on port 3000');
});

Phần health check này rất quan trọng. Nếu pod chỉ kiểm tra process Node.js còn sống mà không đụng tới PostgreSQL hoặc Redis, Kubernetes vẫn đánh dấu pod healthy dù app thực tế không phục vụ được request nào có dữ liệu. Khi đó lỗi sẽ chỉ lộ ra ở phía người dùng.

Với migration, nên có một bước chạy riêng trước khi app nhận traffic. Có team dùng Job độc lập, có team dùng init container chờ database rồi chạy migration. Cách nào cũng được, miễn là migration không âm thầm chạy song song ở mọi replica.

backend:
  initContainers:
    - name: wait-for-postgresql
      image: busybox:1.36
      command:
        - sh
        - -c
        - until nc -z ${DB_HOST} 5432; do echo waiting for postgresql; sleep 2; done
    - name: wait-for-redis
      image: busybox:1.36
      command:
        - sh
        - -c
        - until nc -z ${REDIS_HOST} 6379; do echo waiting for redis; sleep 2; done
Init container và readiness probe chờ PostgreSQL Redis sẵn sàng

Khi đã có endpoint kiểm tra tốt, bạn gắn nó vào readiness và liveness probe. Readiness probe nên phản ánh khả năng phục vụ request thật. Liveness probe thì chỉ nên đủ để phát hiện app bị treo, tránh cấu hình quá gắt khiến pod bị restart liên tục mỗi lúc DB chậm vài giây.

readinessProbe:
  httpGet:
    path: /healthz
    port: 3000
  initialDelaySeconds: 10
  periodSeconds: 5
livenessProbe:
  httpGet:
    path: /healthz
    port: 3000
  initialDelaySeconds: 30
  periodSeconds: 10

Workflow helm dependency update

workflow helm dependency update

Sau khi sửa Chart.yaml, đừng quên cập nhật thư mục charts/ và file lock. Nhiều bạn thêm dependency xong chạy helm install ngay, tới lúc CI/CD build mới phát hiện thiếu chart con hoặc version bị lệch so với máy local.

helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo update
helm dependency update ./fullstack-app
helm template myapp ./fullstack-app -f values.yaml > rendered.yaml
helm lint ./fullstack-app

helm dependency update sẽ tải chart con về thư mục charts/ và tạo hoặc cập nhật Chart.lock. Nếu dự án của bạn cần build lặp lại đúng version đã duyệt, file lock gần như là bắt buộc. Nó giúp các môi trường dùng cùng một tập dependency thay vì mỗi máy kéo một bản khác nhau.

💡 Một workflow khá sạch là: sửa Chart.yaml, chạy helm dependency update, commit luôn Chart.lock, sau đó render thử bằng helm template trước khi deploy thật. Làm vậy bắt được rất nhiều lỗi values và helper template từ sớm.

Cấu hình theo môi trường

cấu hình theo môi trường

Dev, staging và production không nên dùng y hệt một bộ database config. Ở dev, bạn cần gọn nhẹ, reset nhanh, ít tốn tài nguyên. Ở staging, bạn muốn gần production hơn một chút để test migration, connection pool, session và persistence. Còn production thì mục tiêu là an toàn dữ liệu trước tiên.

# values-dev.yaml
postgresql:
  enabled: true
  primary:
    persistence:
      size: 2Gi
redis:
  enabled: true
  master:
    persistence:
      enabled: false
# values-staging.yaml
postgresql:
  enabled: true
  primary:
    persistence:
      size: 8Gi
redis:
  enabled: true
  master:
    persistence:
      enabled: true
      size: 2Gi
# values-prod.yaml
postgresql:
  enabled: false
externalDatabase:
  host: postgresql-prod.example.internal
  port: 5432
  database: appdb
redis:
  enabled: false
externalRedis:
  host: redis-prod.example.internal
  port: 6379

Mấu chốt là chart chính phải hỗ trợ được cả hai mode: dùng dependency nội bộ và dùng external service. Chỗ này thường giải quyết bằng helper template hoặc điều kiện if trong secret/env để chọn endpoint phù hợp. Nhờ vậy bạn không cần duy trì hai chart khác nhau cho cùng một ứng dụng.

Một chi tiết nhiều team hay bỏ sót là connection pool và resource limit cũng cần thay theo môi trường. Dev có thể chỉ chạy một replica backend, ít traffic, PostgreSQL local nhỏ nên pool 5-10 connection là đủ. Nhưng staging và production nếu tăng replica, giữ nguyên pool cũ hoặc tăng bừa quá tay đều có thể gây vấn đề. Tăng quá cao thì database bị ngợp vì tổng số connection từ nhiều pod cộng lại. Tăng quá thấp thì request chờ lâu dù CPU vẫn còn.

Thực tế hơn là bạn để các giá trị như DB_POOL_MAX, timeout, persistence size, resource request và probe timing trong từng file values riêng. Nhìn hơi nhiều biến lúc đầu, nhưng đổi lại bạn kiểm soát được hành vi ứng dụng theo từng môi trường rất rõ. Khi staging bị lỗi, bạn cũng dễ so với production để biết khác ở đâu, thay vì lẫn tất cả trong một file values khổng lồ.

Cấu hình database theo môi trường dev staging production

Secret và persistence

secret và persistence

Với dữ liệu nhạy cảm như password, connection string, session secret, JWT secret hoặc API key, pattern an toàn hơn là tách chúng khỏi values thường. Tối thiểu bạn nên tạo Kubernetes Secret riêng. Tốt hơn nữa là dùng sealed secret, External Secrets Operator hoặc đồng bộ từ secret manager của hạ tầng.

apiVersion: v1
kind: Secret
metadata:
  name: fullstack-app-db
type: Opaque
stringData:
  database-url: postgresql://appuser:supersecret@myapp-postgresql:5432/appdb
---
apiVersion: v1
kind: Secret
metadata:
  name: fullstack-app-redis
type: Opaque
stringData:
  redis-url: redis://:anothersecret@myapp-redis-master:6379/0

Về persistence, PostgreSQL gần như luôn cần PVC nếu bạn chạy trong cluster. Redis thì tùy vai trò. Nếu chỉ làm cache thuần, mất dữ liệu có thể chấp nhận được thì có thể tắt persistence để đơn giản hơn. Nếu Redis dùng cho queue, session quan trọng hoặc rate limit state, bạn nên bật persistence hoặc chuyển hẳn sang dịch vụ ngoài.

Ngoài chuyện có lưu dữ liệu hay không, bạn cũng nên nghĩ tới vòng đời của volume. Nhiều chart mặc định tạo PVC rất tiện, nhưng nếu namespace bị xóa hoặc storage class có chính sách reclaim không phù hợp thì dữ liệu vẫn có thể mất. Ở production, nên kiểm tra rõ storage class đang dùng, snapshot có hỗ trợ hay không, backup chạy theo lịch nào và restore đã từng test chưa. Backup chưa test restore thì vẫn chỉ là cảm giác an tâm.

Chỗ secret cũng tương tự. Đừng chỉ tạo secret rồi coi như xong. Bạn nên thống nhất một pattern xoay vòng mật khẩu, cập nhật secret khi deploy và cách để app reload cấu hình. Nếu backend chỉ đọc secret lúc khởi động, việc đổi password PostgreSQL hoặc Redis sẽ cần rollout pod có kiểm soát. Viết rõ điều này trong runbook sẽ giúp đỡ đau đầu hơn hẳn khi sự cố thật xảy ra lúc nửa đêm.

  • PostgreSQL trong cluster: cần PVC, backup định kỳ, chính sách restore rõ ràng.
  • Redis cache tạm: có thể không cần persistence.
  • Redis session hoặc queue: nên cân nhắc persistence và HA cao hơn.
  • Production nhiều dữ liệu: thường tách database ra khỏi vòng đời app deploy.

Tổng kết

Sau phần này, full-stack chart của bạn đã có thêm hai mảnh ghép quan trọng là PostgreSQL và Redis. Cái được lớn nhất không nằm ở vài dòng dependency, mà ở cách nghĩ đúng vòng đời cho stateful service: khi nào gom chung với app, khi nào tách riêng, cấu hình secret thế nào, readiness probe nên kiểm tra gì, và làm sao để chart vẫn chạy được ở dev lẫn production.

Nếu bạn đang dựng lab để học Helm, cứ bắt đầu bằng dependency cho nhanh. Khi đã quen luồng values, secrets và health check, bạn sẽ thấy việc chuyển sang managed PostgreSQL hoặc Redis bên ngoài không còn khó nữa. Phần tiếp theo sẽ nối tiếp từ đây, tập trung vào lớp routing và expose service ra ngoài để stack full-stack có thể chạy như một ứng dụng hoàn chỉnh hơn.

Faq về database integration

Có nên luôn dùng PostgreSQL dependency trong production không?

Không hẳn. Với production nhỏ, bạn vẫn có thể chạy trong cluster nếu đã có backup, monitoring và quy trình restore tốt. Nhưng về lâu dài, external database hoặc managed service thường dễ kiểm soát hơn.

Redis có bắt buộc phải bật persistence không?

Không. Nếu Redis chỉ làm cache, mất dữ liệu vẫn tự tạo lại được thì có thể tắt. Nếu nó giữ session, queue hoặc dữ liệu tạm nhưng quan trọng với trải nghiệm người dùng, nên cân nhắc bật persistence.

Vì sao pod đã chạy nhưng app vẫn lỗi kết nối database?

Pod chạy không có nghĩa là dependency đã sẵn sàng. PostgreSQL và Redis có thể vẫn đang init. Bạn cần init container hoặc retry logic trong app, đồng thời readiness probe phải kiểm tra được kết nối thật.

Nên truyền từng biến DB hay dùng DATABASE_URL?

Nếu framework và thư viện của bạn hỗ trợ tốt, DATABASE_URLREDIS_URL thường gọn, dễ tái sử dụng cho migration và worker. Truyền từng biến riêng lại tiện hơn nếu cần override chi tiết ở nhiều nơi.

Chart.lock có cần commit vào Git không?

Nên commit. File này giúp cả team và CI/CD kéo đúng version dependency đã được kiểm thử, tránh kiểu máy local cài một bản còn pipeline dùng bản khác.

Chia sẻ:
Bài viết đã được kiểm duyệt bởi AZDIGI Team

Về tác giả

Trần Thắng

Trần Thắng

Chuyên gia tại AZDIGI với nhiều năm kinh nghiệm trong lĩnh vực web hosting và quản trị hệ thống.

Hơn 10 năm phục vụ 80.000+ khách hàng

Bắt đầu dự án web của bạn với AZDIGI