Spring Cloud Config: quản lý cấu hình tập trung
Tiếp đi: quản lý cấu hình tập trung.
Ở bài trước, các service đã tìm thấy nhau qua Eureka. Bây giờ ta xử lý một vấn đề rất thực tế: cấu hình. Thêm config-server vào project spring-cloud-demo.
1. Bài toán: 20 microservices, mỗi service một file config
order-service
payment-service
inventory-service
notification-service
...
Mỗi service có application.yml riêng, đóng gói trong JAR:
server:
port: 8082
spring:
datasource:
url: jdbc:postgresql://localhost:5432/payments
username: ${DB_USERNAME}
password: ${DB_PASSWORD}
payment:
timeout: 5000
retry: 3
Ban đầu không sao. Nhưng khi có 20 service × 3 môi trường (DEV, STAGING, PROD) × nhiều instance, và cần đổi timeout hay feature flag thường xuyên...
2. Vấn đề: config bị phân tán
Muốn đổi payment.timeout từ 5000 thành 3000:
- Config nằm trong JAR → phải sửa code, build, deploy lại.
- Mỗi môi trường một bản → dễ lệch giữa DEV và PROD.
- Không biết ai đổi gì, lúc nào, không rollback được.
3. Giải pháp: Spring Cloud Config
Gồm hai thành phần:
- Config Server: cung cấp API để app lấy config từ nguồn tập trung, thường là Git.
- Config Client: Spring Boot app lấy config từ Config Server lúc khởi động.
Order Service ─┐
Payment Service ─┼──> Config Server :8888 ──> Git repo
User Service ─┘
Config Server không nhất thiết đọc Git từ xa mỗi request, nó giữ local clone và cache.
Tổ chức Git repository:
microservices-config/
│
├── application.yml (chung cho mọi app)
│
├── order-service.yml
├── order-service-dev.yml
├── order-service-prod.yml
│
├── payment-service.yml
├── payment-service-dev.yml
└── payment-service-prod.yml
| File | Ý nghĩa |
|---|---|
application.yml | Cấu hình chung cho nhiều applications |
order-service.yml | Cấu hình mặc định của Order Service |
order-service-dev.yml | Order Service ở DEV |
order-service-prod.yml | Order Service ở PROD |
payment-service-prod.yml | Payment Service ở PROD |
# payment-service.yml
payment:
timeout: 5000
retry: 2
logging:
level:
com.example.payment: INFO
# payment-service-dev.yml
payment:
timeout: 10000
logging:
level:
com.example.payment: DEBUG
# payment-service-prod.yml
payment:
timeout: 3000
logging:
level:
com.example.payment: WARN
Cùng một Payment Service, chạy DEV và PROD nhận config khác nhau. File profile ghi đè file mặc định.
4. Code từng bước
spring-cloud-demo/
├── discovery-server/
├── config-server/ <-- NEW
├── api-gateway/
├── order-service/
└── payment-service/
Bước 1 — Dependency (config-server):
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-server</artifactId>
</dependency>
Version lấy từ Spring Cloud BOM ở parent pom (bài 4.6.1).
Bước 2 — Config (config-server/application.yml):
server:
port: 8888
spring:
application:
name: config-server
cloud:
config:
server:
git:
uri: https://github.com/example/microservices-config
default-label: main
URL minh họa. Repo private thì truyền thông tin xác thực qua biến môi trường hoặc secret manager, không ghi vào file.
Bước 3 — Code Java. Server:
@SpringBootApplication
@EnableConfigServer
public class ConfigServerApplication {
public static void main(String[] args) {
SpringApplication.run(ConfigServerApplication.class, args);
}
}
Client (payment-service) thêm dependency:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
# payment-service/application.yml
spring:
application:
name: payment-service # dùng để tìm file payment-service*.yml
profiles:
active: dev
config:
import: "configserver:http://localhost:8888"
Nếu Config Server không chạy, app sẽ không khởi động được. Muốn cho phép chạy thiếu Config Server thì dùng optional:configserver:....
@Service
public class PaymentService {
@Value("${payment.timeout}")
private int paymentTimeout;
public void processPayment() {
System.out.println("Payment timeout: " + paymentTimeout);
}
}
Bước 4 — Test bằng curl:
curl http://localhost:8888/payment-service/dev
{
"name": "payment-service",
"profiles": ["dev"],
"label": "main",
"propertySources": [
{ "name": "payment-service-dev.yml",
"source": { "payment.timeout": 10000 } },
{ "name": "payment-service.yml",
"source": { "payment.timeout": 5000, "payment.retry": 2 } }
]
}
Endpoint theo dạng /{application}/{profile}/{label}: /payment-service/prod/main, /order-service/dev/main... Chạy Payment với profile dev in ra Payment timeout: 10000, với prod in ra 3000.
5. Cách hoạt động
Payment Service khởi động (profile=dev)
|
| GET /payment-service/dev
v
Config Server :8888
|
| git pull (local clone)
v
Git: application.yml + payment-service.yml + payment-service-dev.yml
|
v
Trả về propertySources (dev ưu tiên cao hơn mặc định)
|
v
Payment Service đưa vào Environment → tạo bean với config đó
Điểm quan trọng: bean được tạo một lần với config đã nạp. Git đổi sau đó thì bean không tự đổi.
6. So sánh: config trong JAR vs Config Server
| Tiêu chí | application.yml trong JAR | Spring Cloud Config |
|---|---|---|
| Đổi config | Build + deploy lại | Sửa Git + refresh / restart |
| Lịch sử thay đổi | Lẫn trong commit code | Git history riêng, rollback được |
| Nhiều môi trường | Nhiều file rải trong từng repo | Một repo, tách theo profile |
| Phụ thuộc thêm | Không | Phải vận hành Config Server |
7. Khi nào nên / không nên dùng
Nên: nhiều microservices, nhiều môi trường, muốn version control config trên Git, cần refresh có kiểm soát, chạy trên VM / bare metal / hybrid.
Không cần: chỉ vài app, config đơn giản ít đổi, hoặc đã chạy hoàn toàn trên Kubernetes và ConfigMap/Secret đủ dùng. Thêm Config Server là thêm một thứ phải vận hành.
payment.timeout trên Git rồi push, Payment Service đang chạy có tự nhận 3000 không?Sửa Git xong push lên thì service đang chạy tự cập nhật luôn đúng không?
Chưa đúng. Mặc định là không. Config Server đọc được giá trị mới từ Git, nhưng Payment Service đã tạo bean với giá trị cũ.
Muốn cập nhật mà không restart, dùng cơ chế refresh. Thêm Actuator:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
management:
endpoints:
web:
exposure:
include: health,info,refresh
Đánh dấu bean cần nhận giá trị mới bằng @RefreshScope:
@RefreshScope
@Service
public class PaymentService {
@Value("${payment.timeout}")
private int timeout;
public int getTimeout() {
return timeout;
}
}
curl -X POST http://localhost:8082/actuator/refresh
# ["payment.timeout"]
POST /actuator/refresh
|
v
Nạp lại Environment từ Config Server
|
v
Bean @RefreshScope bị hủy
|
v
Lần gọi tiếp theo: bean tạo lại với timeout = 3000
Bean không có @RefreshScope vẫn giữ giá trị cũ (riêng @ConfigurationProperties thì được rebind). Và đừng expose /actuator/refresh ra Internet, phải giới hạn truy cập.
Nhưng có 100 instance thì phải gọi refresh 100 lần à?
Đúng là vấn đề đó. Đây là lúc dùng Spring Cloud Bus: nối các instance qua message broker (RabbitMQ hoặc Kafka) để phát sự kiện quản trị, trong đó có refresh.
Payment Service: 30 instances
Order Service: 40 instances
User Service: 30 instances
Developer push Git
|
| webhook / CI gọi
v
POST /actuator/busrefresh (1 instance bất kỳ)
|
v
RabbitMQ / Kafka ── RefreshRemoteApplicationEvent ──┐
| | |
v v v
payment #1..30 order #1..40 user #1..30
refresh refresh refresh
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>
spring:
rabbitmq:
host: localhost
port: 5672
username: ${RABBITMQ_USERNAME}
password: ${RABBITMQ_PASSWORD}
management:
endpoints:
web:
exposure:
include: health,busrefresh
curl -X POST http://localhost:8082/actuator/busrefresh
Lưu ý: Bus không tự phát hiện Git push. Phải có webhook hoặc automation kích hoạt. Endpoint này cũng phải được bảo vệ.
Vậy chạy trên Kubernetes thì dùng ConfigMap được rồi, còn cần Config Server không?
Đúng rồi, nhiều trường hợp không cần. Nếu mọi app chạy trên Kubernetes, ConfigMap và Secret đáp ứng phần lớn nhu cầu.
| Tiêu chí | Spring Cloud Config | Kubernetes ConfigMap / Secret |
|---|---|---|
| Nguồn config | Git, filesystem, Vault... | Kubernetes API (YAML quản lý qua GitOps được) |
| Phạm vi | Ứng dụng, đa môi trường, đa nền tảng | Cluster / namespace |
| Dynamic update | Refresh / Bus | Volume tự cập nhật; env var thì cần restart Pod |
| Secret | Cần backend bảo mật như Vault | Secret, cần RBAC và encryption at rest |
| Phù hợp | VM, hybrid, không có K8s | Hệ thống chạy hoàn toàn trên K8s |
Một điểm hay bị nhầm: ConfigMap cập nhật không có nghĩa Spring Boot tự reload. Truyền qua env var thì container đang chạy không đổi, cần restart hoặc cơ chế reload (ví dụ Spring Cloud Kubernetes). Chi tiết ở bài 4.6.9.
Phỏng vấn hỏi về Spring Cloud Config thì trả lời sao?
Q1. Spring Cloud Config là gì? Config Server khác Config Client thế nào?
🇻🇳 Tiếng Việt:
Spring Cloud Config quản lý cấu hình tập trung cho hệ phân tán. Config Server đọc config từ nguồn tập trung, thường là Git, và cung cấp qua API dạng
/{application}/{profile}/{label}. Config Client là các Spring Boot app, lúc khởi động dùngspring.application.namevà profile để lấy đúng config của mình.
🇬🇧 English:
Spring Cloud Config provides centralized configuration for distributed systems. The Config Server reads configuration from a central source, typically Git, and exposes it via
/{application}/{profile}/{label}. Config Clients are Spring Boot apps that, at startup, usespring.application.nameand the active profile to fetch their configuration.
Q2. Sửa config trên Git, service đang chạy có tự cập nhật không?
🇻🇳 Tiếng Việt:
Không. Bean đã được tạo với giá trị cũ. Em gọi
/actuator/refreshđể nạp lại Environment, và bean cần giá trị mới phải đánh dấu@RefreshScopehoặc dùng@ConfigurationProperties. Nếu nhiều instance thì dùng Spring Cloud Bus để phát refresh tới tất cả. Endpoint refresh phải được bảo vệ, không expose ra ngoài.
🇬🇧 English:
No. Beans were created with the old values. I call
/actuator/refreshto reload the Environment, and beans that need new values must be@RefreshScopeor use@ConfigurationProperties. With many instances, I use Spring Cloud Bus to broadcast the refresh to all of them. Refresh endpoints must be secured and not publicly exposed.
Q3. Spring Cloud Bus là gì?
🇻🇳 Tiếng Việt:
Spring Cloud Bus nối các instance qua message broker như RabbitMQ hoặc Kafka để phát sự kiện, ví dụ refresh config. Gọi
/actuator/busrefreshtrên một instance thì mọi instance nhận được sự kiện. Nó không tự phát hiện Git push, cần webhook hoặc CI kích hoạt.
🇬🇧 English:
Spring Cloud Bus links application instances through a message broker such as RabbitMQ or Kafka to broadcast events, such as configuration refresh. Calling
/actuator/busrefreshon one instance propagates the event to all of them. It does not detect Git pushes by itself; a webhook or CI pipeline must trigger it.
Q4. Dùng Kubernetes rồi có cần Spring Cloud Config không?
🇻🇳 Tiếng Việt:
Không bắt buộc. ConfigMap và Secret thường đủ khi mọi thứ chạy trên Kubernetes. Spring Cloud Config vẫn có ích khi cần config tập trung trên Git cho nhiều nền tảng, như VM và K8s cùng lúc, hoặc cần cơ chế refresh của Spring.
🇬🇧 English:
Not necessarily. ConfigMaps and Secrets are usually enough when everything runs on Kubernetes. Spring Cloud Config is still useful when we need Git-based centralized configuration across platforms, such as VMs and Kubernetes together, or Spring's refresh mechanism.
Cách nhớ ngắn gọn
- Server đọc Git, Client lấy theo
name+profile. - Git đổi ≠ app đổi: cần refresh +
@RefreshScope. - Nhiều instance → Bus qua RabbitMQ/Kafka, kích hoạt bằng webhook.
- Toàn bộ trên K8s → ConfigMap/Secret thường là đủ.
- Không bao giờ để password trong repo config.
Tự kiểm tra
Payment chạy profile prod lấy những file nào từ Git?
application.yml, payment-service.yml, payment-service-prod.yml; file prod ưu tiên cao nhất.
Config Server sập lúc Payment khởi động thì sao?
Với configserver:... app không khởi động được. Dùng optional:configserver:... để cho phép chạy tiếp.
Gọi /actuator/refresh nhưng giá trị @Value vẫn cũ, vì sao?
Bean không có @RefreshScope nên không được tạo lại.
10 instance, đổi timeout trên Git và push. Chuyện gì xảy ra?
Không instance nào tự cập nhật. Cần refresh từng cái, busrefresh qua Bus, hoặc restart.
ConfigMap truyền qua env var được sửa, Pod đang chạy có nhận không?
Không. Env var cố định lúc container khởi động, cần restart Pod hoặc cơ chế reload.