# 🔐 Quản lý biến môi trường và secrets

By Better Wise

> Sign in to view the full content of this lesson.

Bạn vừa tách xong dev / staging / production ở bài trước. Giờ ba môi trường đó cần cấu hình khác nhau (database khác, API key khác, URL khác), và bạn phải giữ chúng tách rời mà không lộ secrets. Đây là bài quyết định: nếu bước này hỏng, bạn không rollback được bằng Git, vì secret đã lỡ commit thì nó nằm trong lịch sử mãi mãi.

Sau bài này bạn sẽ:

- Đặt biến môi trường cho từng môi trường, không hardcode.
- Phân biệt biến cấu hình thường và secret.
- Giữ secrets ngoài mã nguồn, không commit lên Git.
- Có quy trình xoay vòng (rotate) khi secret bị lộ.

## Hai loại biến, đừng gộp làm một

**Biến cấu hình** là thứ không nguy hiểm nếu lộ: `APP_ENV`, `LOG_LEVEL`, `API_BASE_URL`, `FEATURE_X_ENABLED`. **Secret** là thứ lộ ra là mất tiền hoặc mất dữ liệu: `DATABASE_URL` (có mật khẩu), `STRIPE_SECRET_KEY`, `JWT_SIGNING_KEY`, `SMTP_PASSWORD`.

Quy tắc của release engineer: nếu ai đó đọc được giá trị mà bạn phải đổi mật khẩu ngay, đó là secret. Còn lại là cấu hình.

Cả hai đều đi qua biến môi trường, nhưng cách lưu khác nhau hoàn toàn.

### Biến cấu hình: một file `.env` cho mỗi môi trường

```bash
# .env.example  (commit file này, không có giá trị thật)
APP_ENV=development
LOG_LEVEL=debug
API_BASE_URL=http://localhost:3000
DATABASE_URL=postgres://user:pass@localhost:5432/app
```

```bash
# .env.development   (KHÔNG commit)
API_BASE_URL=http://localhost:3000
DATABASE_URL=postgres://dev:devpass@localhost:5432/app_dev

# .env.staging       (KHÔNG commit)
API_BASE_URL=https://staging.example.com
DATABASE_URL=postgres://stg_user:...@stg-db:5432/app_staging

# .env.production    (KHÔNG commit, thường không tồn tại trên máy dev)
API_BASE_URL=https://example.com
DATABASE_URL=postgres://prod_user:...@prod-db:5432/app_prod
```

`.gitignore` phải chặn từ đầu, trước khi bạn tạo file:

```gitignore
.env
.env.*
!.env.example
```

Dấu `!` ở dòng cuối là ngoại lệ: file mẫu được commit, để người mới clone về biết cần biến gì.

> **Warning:** 
> Thêm `.env` vào `.gitignore` sau khi đã commit thì vô nghĩa. Git vẫn giữ file trong lịch sử. Phải thêm trước commit đầu tiên, hoặc phải xóa khỏi toàn bộ lịch sử và rotate mọi secret trong đó.

### Secret: không bao giờ nằm trong file trên máy

Ở production, secret nên đến từ nơi lưu trữ chuyên dụng, không phải file `.env` trên VPS:

- **Nền tảng PaaS** (Vercel, Render, Fly.io, Railway): nhập secret trong dashboard, inject vào runtime lúc build/khởi động.
- **CI/CD** (GitHub Actions, GitLab CI): secret lưu trong phần Settings > Secrets của repo, chỉ hiện trong log dưới dạng `***`.
- **Secret manager** (AWS Secrets Manager, Doppler, Vault): dùng khi có nhiều service và cần audit ai đọc secret nào.

Ứng dụng đọc cùng một tên biến, không cần biết secret đến từ đâu:

```js
// config.js
function required(name) {
  const value = process.env[name];
  if (!value) throw new Error(`Thiếu biến môi trường: ${name}`);
  return value;
}

export const config = {
  env: process.env.APP_ENV ?? 'development',
  apiBaseUrl: required('API_BASE_URL'),
  databaseUrl: required('DATABASE_URL'),
  jwtKey: required('JWT_SIGNING_KEY'),
};
```

Hàm `required` là chốt chặn: thiếu biến thì app chết ngay lúc khởi động, chứ không chạy nửa vời rồi lỗi lúc 2 giờ sáng. Fail fast tốt hơn fail lặng lẽ.

**Bài tập 1:** Bạn thấy dòng này trong repo: `const key = "sk_live_51H...";`. Viết ba việc phải làm, theo đúng thứ tự.

**Xem gợi ý**

1. Coi secret đó là **đã lộ**: rotate (thu hồi và tạo key mới) ngay trên dashboard của dịch vụ, vì nó đã nằm trong lịch sử Git.
2. Chuyển sang đọc từ `process.env.STRIPE_SECRET_KEY` và thêm tên biến vào `.env.example`.
3. Xóa khỏi mã nguồn, commit; nếu repo public thì xử lý cả lịch sử Git (ví dụ `git filter-repo`) và kiểm tra log CI xem key có từng bị in ra không.

Thứ tự quan trọng: rotate trước, dọn sau. Dọn mã nguồn không làm secret cũ trở nên vô hại.

## Kiểm tra cấu hình trước khi deploy

Trước mỗi lần deploy, bạn cần biết môi trường đích đã có đủ biến chưa. Một script nhỏ, chạy trong CI:

```bash
#!/usr/bin/env bash
# scripts/check-env.sh
set -euo pipefail

required_vars=(APP_ENV API_BASE_URL DATABASE_URL JWT_SIGNING_KEY)

for name in "${required_vars[@]}"; do
  if [ -z "${!name:-}" ]; then
    echo "FAIL: thiếu biến $name cho môi trường ${APP_ENV:-unknown}"
    exit 1
  fi
done

echo "OK: đủ $((${#required_vars[@]})) biến cho $APP_ENV"
```

Điểm cần nhớ: script chỉ in **tên** biến, không in **giá trị**. Log CI thường được lưu lại và chia sẻ cho cả team.

> **Important:** 
> Trước khi cho phép deploy, tự hỏi: nếu secret bị lộ ở bước này, tôi rotate trong bao lâu? Nếu câu trả lời là "không biết", bạn chưa sẵn sàng deploy.

**Bài tập 2:** Đoạn script trên in ra `FAIL: thiếu biến JWT_SIGNING_KEY cho production` khi chạy trên staging. Nêu hai nguyên nhân khả dĩ và cách kiểm tra từng cái.

**Xem gợi ý**

- Nguyên nhân 1: secret chưa được thêm vào phần Settings > Secrets của môi trường staging (hoặc tên bị đặt khác, ví dụ `JWT_KEY`). Kiểm tra bằng cách liệt kê tên secret trong dashboard CI, so khớp từng ký tự.
- Nguyên nhân 2: job CI chạy ở environment `production` do cấu hình sai, nên `APP_ENV` bị lẫn. Kiểm tra bằng cách in `echo "env=$APP_ENV"` ở đầu job (an toàn vì không phải secret).

Bài học: thông báo lỗi phải nói rõ **môi trường nào**, nếu không bạn sẽ đi sửa sai chỗ.

## Ba thói quen giữ secrets ngoài mã nguồn

## Những cái bẫy khiến secret rò rỉ

- **Commit `.env.example` với giá trị thật.** File mẫu chỉ chứa placeholder như `DATABASE_URL=postgres://user:pass@host:5432/db`. Không bao giờ copy giá trị staging vào đó.
- **In cả object config khi debug.** `console.log(config)` đẩy toàn bộ secret vào log. Log thường được lưu lâu hơn bạn tưởng.
- **Dùng chung một secret cho cả ba môi trường.** Lộ staging là lộ production. Mỗi môi trường phải có credential riêng, kể cả key của bên thứ ba (dùng test key ở dev/staging).
- **Nghĩ rằng repo private là đủ an toàn.** Người rời team vẫn còn bản clone cũ. Secret phải rotate khi có người rời đi.
- **Quên rotate sau khi lộ.** Xóa khỏi Git không thu hồi được key đã bị bot quét. Với repo public, secret thường bị quét trong vài phút.
- **Đặt biến trong Dockerfile.** `ENV STRIPE_KEY=...` nằm vĩnh viễn trong image layer. Truyền secret lúc runtime thay vì lúc build.

Quy tắc của tôi: nếu việc đọc một biến môi trường cần bạn mở dashboard của ba nơi khác nhau, cấu hình đó đã quá phức tạp để vận hành an toàn. Đơn giản hóa trước khi thêm môi trường thứ tư.

## Chốt lại

Biến cấu hình và secret đều đi qua biến môi trường, nhưng secret phải sống ngoài mã nguồn, được inject từ nền tảng hoặc CI, và được rotate ngay khi nghi ngờ lộ. App phải fail fast khi thiếu biến, và script kiểm tra chỉ in tên biến, không in giá trị. Nhớ ba việc: `.gitignore` trước commit đầu, mỗi môi trường một credential, rotate trước khi dọn.

Bài tiếp theo: sau khi cấu hình đã đúng, bạn sẽ viết **smoke test** chạy ngay sau mỗi lần deploy, để biết môi trường vừa deploy có thật sự sống hay không trước khi báo cho người dùng.

Front: Secret là gì?
Back: Giá trị mà nếu lộ ra bạn phải đổi mật khẩu hoặc thu hồi key ngay, ví dụ DATABASE_URL, JWT_SIGNING_KEY.

Front: Biến cấu hình khác secret ở điểm nào?
Back: Biến cấu hình lộ ra không nguy hiểm (APP_ENV, LOG_LEVEL, API_BASE_URL); secret thì có.

Front: File nào trong bộ .env nên được commit?
Back: .env.example, chỉ chứa placeholder, để người mới biết cần biến gì.

Front: Dòng .gitignore chặn toàn bộ .env nhưng giữ file mẫu?
Back: `.env`, `.env.*`, rồi `!.env.example`.

Front: Tại sao thêm .env vào .gitignore sau khi đã commit là vô ích?
Back: Vì Git vẫn giữ file trong lịch sử; phải xử lý lịch sử và rotate secret.

Front: Secret ở production nên được lưu ở đâu?
Back: Trong secret settings của PaaS hoặc CI/CD, hoặc secret manager; không nằm trong file trên VPS.

Front: Vì sao app nên fail fast khi thiếu biến môi trường?
Back: Để chết ngay lúc khởi động thay vì lỗi âm thầm lúc đang chạy.

Front: Script kiểm tra biến môi trường nên in gì?
Back: Chỉ tên biến và môi trường, không bao giờ in giá trị.

Front: Sai lầm khi dùng chung secret cho dev, staging, production là gì?
Back: Lộ một môi trường là lộ tất cả; mỗi môi trường cần credential riêng.

Front: Vì sao không đặt secret bằng ENV trong Dockerfile?
Back: Secret nằm vĩnh viễn trong image layer; phải truyền lúc runtime.

Front: console.log(config) nguy hiểm ở đâu?
Back: Đẩy toàn bộ secret vào log, mà log thường được lưu lâu và chia sẻ rộng.

Front: Việc đầu tiên khi phát hiện secret bị commit?
Back: Rotate secret đó ngay, rồi mới dọn mã nguồn và lịch sử Git.

Khi phát hiện một secret đã bị commit, việc đầu tiên phải làm là _____ secret đó, không phải xóa file.
File được commit để làm mẫu cho người mới là _____, và nó chỉ chứa placeholder.
Ở production, secret nên được inject từ dashboard của _____ hoặc phần Secrets của CI/CD.
Hàm kiểm tra biến môi trường nên _____ khi thiếu biến, tức là báo lỗi ngay lúc khởi động.
Script check-env chỉ được in _____ biến, tuyệt đối không in giá trị.
Mỗi môi trường phải dùng credential _____, để lộ staging không kéo theo production.

---

## Contents

Part of [🚀 Ra mắt an toàn cho app vibe-code: staging, smoke test và rollback](https://everwise.study/courses/ra-mat-an-toan-cho-app-vibe-code-staging-smoke-test-va-rollback.md)

- **🔧 Môi trường và cấu hình an toàn**
  - [🏗️ Tách môi trường dev, staging, production](https://everwise.study/courses/ra-mat-an-toan-cho-app-vibe-code-staging-smoke-test-va-rollback/tach-moi-truong-dev-staging-production.md)
  - [🔐 Quản lý biến môi trường và secrets](https://everwise.study/courses/ra-mat-an-toan-cho-app-vibe-code-staging-smoke-test-va-rollback/quan-ly-bien-moi-truong-va-secrets.md) (sign-in required) — this lesson
  - [🔥 Smoke test sau mỗi lần deploy](https://everwise.study/courses/ra-mat-an-toan-cho-app-vibe-code-staging-smoke-test-va-rollback/smoke-test-sau-moi-lan-deploy.md)
  - [✅ Checkpoint: Môi trường, secrets và smoke test](https://everwise.study/courses/ra-mat-an-toan-cho-app-vibe-code-staging-smoke-test-va-rollback/checkpoint-moi-truong-secrets-smoke-test.md)
- **🧪 Kiểm thử và kiểm soát phát hành**
  - [🎭 E2E cơ bản với Playwright](https://everwise.study/courses/ra-mat-an-toan-cho-app-vibe-code-staging-smoke-test-va-rollback/e2e-co-ban-voi-playwright.md)
  - [🚩 Feature flag để tắt tính năng lỗi](https://everwise.study/courses/ra-mat-an-toan-cho-app-vibe-code-staging-smoke-test-va-rollback/feature-flag-de-tat-tinh-nang-loi.md)
  - [💾 Sao lưu và rollback trước khi deploy](https://everwise.study/courses/ra-mat-an-toan-cho-app-vibe-code-staging-smoke-test-va-rollback/sao-luu-va-rollback-truoc-deploy.md)
  - [📝 Checkpoint: E2E, feature flag và rollback](https://everwise.study/courses/ra-mat-an-toan-cho-app-vibe-code-staging-smoke-test-va-rollback/checkpoint-e2e-feature-flag-rollback.md)
- **🚦 Go-live và runbook phát hành**
  - [📋 Checklist go-live ngắn gọn](https://everwise.study/courses/ra-mat-an-toan-cho-app-vibe-code-staging-smoke-test-va-rollback/checklist-go-live-ngan-gon.md)
  - [🧯 Runbook phát hành và xử lý sự cố sau deploy](https://everwise.study/courses/ra-mat-an-toan-cho-app-vibe-code-staging-smoke-test-va-rollback/runbook-phat-hanh-va-xu-ly-su-co-sau-deploy.md) (premium content)
  - [🏁 Checkpoint: Go-live và runbook](https://everwise.study/courses/ra-mat-an-toan-cho-app-vibe-code-staging-smoke-test-va-rollback/checkpoint-go-live-va-runbook.md) (premium content)
- [Course cheatsheet](https://everwise.study/courses/ra-mat-an-toan-cho-app-vibe-code-staging-smoke-test-va-rollback/cheatsheet.md) (premium content)
