Hướng dẫn triển khai Feature Flags với Unleash trong Node.js: Bật tắt tính năng tức thì, không cần redeploy

Development tutorial - IT technology blog
Development tutorial - IT technology blog

Bối cảnh & Vấn đề thực tế

Khoảng 2 năm trước, mỗi đợt release tính năng mới lên production của team mình đều khá áp lực. Quy trình kinh điển: merge code vào main, kích hoạt CI/CD, build Docker image rồi cập nhật cụm Kubernetes. Nếu chẳng may dính lỗi memory leak hoặc câu query làm khóa bảng PostgreSQL, cả team lại vội vàng revert commit và build lại từ đầu. Quá trình rollback này ngốn mất 15 đến 20 phút. Trong ngần ấy thời gian, hàng trăm đơn hàng đã bị gián đoạn.

Vấn đề cốt lõi: chúng ta thường gộp chung hai khái niệm tách biệt.

  • Deployment: Đưa mã nguồn mới lên server chạy an toàn.
  • Release: Mở tính năng đó cho người dùng cuối trải nghiệm.

Tách rời hai bước này chính là lý do Feature Flags (hay Feature Toggles) ra đời. Thay vì gom nhánh code cả tháng trời, 5 anh em trong team chuyển sang merge liên tục vào nhánh chính mỗi ngày (Trunk-based Development). Mọi đoạn code mới đều bọc trong một Feature Flag ở trạng thái OFF. Khi muốn test nội bộ hoặc mở thử cho 5% khách hàng, tech lead chỉ cần gạt cờ trên UI. Thời gian xử lý sự cố giảm từ 20 phút xuống còn đúng 1 cú click chuột trong 1 giây.

Về công cụ, LaunchDarkly chạy rất mượt nhưng chi phí từ $8–$10/user/tháng khá tốn kém với startup. Flagsmith cũng ổn, nhưng mình chọn Unleash vì mã nguồn mở, cho phép self-host qua Docker để kiểm soát dữ liệu nội bộ. Điểm cộng lớn nhất là SDK Node.js của Unleash đánh giá cờ trực tiếp trên RAM, latency chỉ dưới 0.1ms cho mỗi lần gọi.

Cài đặt môi trường

Hệ thống gồm hai khối riêng biệt: Unleash Server (UI quản trị và lưu trữ cấu hình) và Unleash Client SDK nhúng trực tiếp trong code Node.js.

1. Chạy Unleash Server bằng Docker Compose

Dưới đây là file docker-compose.yml chuẩn để dựng Unleash kèm cơ sở dữ liệu PostgreSQL 15:

services:
  db:
    image: postgres:15-alpine
    environment:
      POSTGRES_DB: unleash
      POSTGRES_USER: unleash_user
      POSTGRES_PASSWORD: secret_db_password
    volumes:
      - unleash_db_data:/var/lib/postgresql/data
    restart: unless-stopped

  unleash:
    image: unleashorg/unleash-server:latest
    ports:
      - "4242:4242"
    environment:
      DATABASE_URL: postgres://unleash_user:secret_db_password@db:5432/unleash
      DATABASE_SSL: "false"
      LOG_LEVEL: "warn"
      INIT_FRONTEND_API_TOKENS: "default:development.unleash-insecure-frontend-api-token"
      INIT_CLIENT_API_TOKENS: "default:development.unleash-insecure-client-api-token"
    depends_on:
      - db
    restart: unless-stopped

volumes:
  unleash_db_data:

Khởi động dịch vụ:

docker compose up -d

Sau khoảng 15 giây, mở trình duyệt vào http://localhost:4242. Thông tin đăng nhập mặc định:

  • Username: admin
  • Password: unleash4all (hệ thống sẽ yêu cầu đổi mật khẩu mới ngay lần đầu)

2. Cài đặt SDK vào project Node.js

Chạy lệnh cài đặt bộ thư viện cần thiết trong thư mục backend Express:

npm install unleash-client express dotenv

Cấu hình và Tích hợp

1. Tạo API Token trên Unleash

Để server Node.js kéo được trạng thái cờ, bạn cần tạo một token phân quyền:

  1. Truy cập dashboard Unleash > Settings > API access.
  2. Chọn New API token.
  3. Chọn loại Server-side SDK (Client), gán project default và môi trường development.
  4. Lưu lại chuỗi token hiển thị trên màn hình.

Tiếp theo, vào mục Feature toggles, tạo cờ mới có tên new-checkout-flow.

2. Khởi tạo Unleash Client dạng Singleton

SDK của Unleash hoạt động theo cơ chế In-memory & Background polling. Cứ mỗi vài giây, client ngầm gửi một request kéo toàn bộ rules về lưu tại RAM. Khi API gọi hàm kiểm tra, logic chạy hoàn toàn trên bộ nhớ local. Ứng dụng không phải gửi thêm request HTTP nào ra ngoài, đảm bảo tốc độ phản hồi cực nhanh.

Tạo file src/unleash.js:

const { initialize } = require('unleash-client');

let unleashInstance = null;

function getUnleash() {
  if (!unleashInstance) {
    unleashInstance = initialize({
      url: process.env.UNLEASH_URL || 'http://localhost:4242/api/',
      appName: 'payment-service',
      environment: process.env.NODE_ENV || 'development',
      customHeaders: {
        Authorization: process.env.UNLEASH_API_TOKEN || '*:development.unleash-insecure-client-api-token',
      },
      // Đồng bộ rule mới mỗi 5 giây khi dev (prod nên để 10000 - 15000ms)
      refreshInterval: 5000,
      // Gửi số liệu thống kê hit/miss về server mỗi 30 giây
      metricsInterval: 30000,
    });

    unleashInstance.on('ready', () => {
      console.log('[Unleash] SDK đã nạp rule vào RAM thành công.');
    });

    unleashInstance.on('error', (err) => {
      console.error('[Unleash] Lỗi đồng bộ cấu hình:', err.message);
    });
  }
  return unleashInstance;
}

module.exports = { getUnleash };

3. Áp dụng Flag vào Endpoint thanh toán

Dưới đây là file src/app.js thể hiện cách bọc logic nghiệp vụ bằng flag:

const express = require('express');
const { getUnleash } = require('./unleash');

const app = express();
app.use(express.json());

const unleash = getUnleash();

app.post('/api/checkout', (req, res) => {
  const userId = req.headers['x-user-id'] || 'anonymous';
  const userEmail = req.headers['x-user-email'] || '';

  // Ngữ cảnh để Unleash đánh giá điều kiện phân nhóm
  const context = {
    userId: userId,
    properties: {
      email: userEmail,
      plan: req.body.plan || 'free',
    },
  };

  // Kiểm tra trạng thái cờ với fallback an toàn là false
  const isNewFlowActive = unleash.isEnabled('new-checkout-flow', context, false);

  if (isNewFlowActive) {
    // Luồng thanh toán Stripe Elements mới
    return res.json({
      version: 'v2',
      gateway: 'stripe-one-click',
      orderId: 'ORD-2026-NEW',
    });
  }

  // Luồng thanh toán cũ đã kiểm chứng ổn định
  return res.json({
    version: 'v1',
    gateway: 'standard-redirect',
    orderId: 'ORD-2026-LEGACY',
  });
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`Backend lắng nghe tại port ${PORT}`);
});

Hãy lưu ý đối số thứ ba trong hàm unleash.isEnabled('new-checkout-flow', context, false). Đây là giá trị fallback an toàn. Nếu Unleash Server gặp sự cố mạng hoặc container bị restart, SDK sẽ trả về ngay false. Ứng dụng tự động dùng luồng cũ mà không ném ra exception hay làm crash tiến trình.

4. Phân phối tính năng với Activation Strategies

Unleash không chỉ có bật/tắt toàn bộ. Giá trị thực tế nằm ở các chiến lược phân phối động cấu hình trực tiếp trên UI:

  • Gradual Rollout (Canary testing): Cho phép mở cờ cho 10%, 25%, rồi nâng dần lên 100% người dùng. Thuật toán MurmurHash3 dựa trên userId giữ kết quả nhất quán. User A khi đã thuộc nhóm 10% sẽ luôn trải nghiệm giao diện mới ở các lần đăng nhập sau.
  • Targeting theo User ID: Chỉ định danh sách tài khoản cụ thể (ví dụ dev_quang, qa_lan) để đội ngũ nội bộ test trước trên production.
  • Custom Constraints: Lọc theo điều kiện mở rộng, ví dụ domain email kết thúc bằng @company.com hoặc app version từ 2.4.0 trở lên.

Kiểm tra và Vận hành

1. Test chuyển đổi luồng tức thì

Khởi động Express server:

node src/app.js

Gửi request kiểm tra khi cờ new-checkout-flow đang TẮT:

curl -X POST http://localhost:3000/api/checkout \
  -H "Content-Type: application/json" \
  -H "x-user-id: user_123"

Phản hồi trả về bản v1:

{"version":"v1","gateway":"standard-redirect","orderId":"ORD-2026-LEGACY"}

Tiếp theo, vào dashboard Unleash và gạt cờ sang BẬT. Đợi 5 giây để SDK hoàn tất chu kỳ poll, sau đó chạy lại lệnh curl vừa rồi. Kết quả đổi sang bản v2 ngay lập tức:

{"version":"v2","gateway":"stripe-one-click","orderId":"ORD-2026-NEW"}

Toàn bộ quá trình chuyển luồng hoàn tất trong chớp mắt. Không có kết nối HTTP nào bị ngắt quãng. Không có container nào phải reload.

2. Đo lường hiệu quả qua Tab Metrics

Mỗi lần hàm isEnabled được gọi, SDK tự gom metric và gửi định kỳ về server. Mở chi tiết cờ và chuyển sang tab Metrics, bạn sẽ theo dõi được:

  • Số lượt trúng nhánh Yes so với nhánh No trong 1 giờ qua.
  • Lưu lượng request thực tế phân bổ giữa hai phiên bản code.
  • Phát hiện sớm các cờ rác đã bật 100% quá lâu nhưng chưa được dọn dẹp khỏi mã nguồn.

3. Ba nguyên tắc tránh biến Feature Flags thành bẫy kỹ thuật

Sau hơn 6 tháng áp dụng thực tế trên hệ thống xử lý khoảng 50.000 đơn/ngày, team mình rút ra 3 bài học đắt giá:

  • Quy ước tiền tố tên cờ: Phân loại rõ mục đích ngay từ đầu. Dùng release- cho tính năng mới cần rollout dần, killswitch- cho công tắc ngắt khẩn cấp khi tích hợp đối tác thứ ba bị lỗi, và experiment- cho các thử nghiệm A/B testing ngắn hạn.
  • Xóa flag triệt để sau khi launch: Flag sinh ra để giảm rủi ro, nhưng nếu để quá lâu sẽ làm phình to nợ kỹ thuật (technical debt). Khi một tính năng đã chạy ổn định 100% trong 2 tuần, hãy tạo ngay ticket trong sprint kế tiếp để xóa khối if/else và gỡ flag khỏi Unleash.
  • Ưu tiên tính độc lập của flag: Hạn chế tối đa việc lồng các flag vào nhau (nested flags). Code lồng 2–3 tầng cờ tính năng sẽ khiến việc viết unit test và tái hiện bug trở thành cơn ác mộng.

Nhờ Unleash, việc đưa tính năng mới lên production không còn là canh bạc mỗi tối thứ Sáu. Khi mọi rủi ro đều có thể ngắt trong vòng 1 giây, cả đội phát triển có thể tự tin đẩy nhanh tốc độ chuyển giao sản phẩm.

Share: