Spring Cloud Config: quản lý cấu hình tập trung

5 câu hỏi~20 phútTừ khóa: Config Server, Config Client, Git backend, @RefreshScope, /actuator/refresh, Spring Cloud Bus, ConfigMap

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:

3. Giải pháp: Spring Cloud Config

Gồm hai thành phần:

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.ymlCấu hình chung cho nhiều applications
order-service.ymlCấu hình mặc định của Order Service
order-service-dev.ymlOrder Service ở DEV
order-service-prod.ymlOrder Service ở PROD
payment-service-prod.ymlPayment 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 JARSpring Cloud Config
Đổi configBuild + deploy lạiSửa Git + refresh / restart
Lịch sử thay đổiLẫn trong commit codeGit history riêng, rollback được
Nhiều môi trườngNhiều file rải trong từng repoMột repo, tách theo profile
Phụ thuộc thêmKhôngPhả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.

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 ConfigKubernetes ConfigMap / Secret
Nguồn configGit, 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ảngCluster / namespace
Dynamic updateRefresh / BusVolume tự cập nhật; env var thì cần restart Pod
SecretCần backend bảo mật như VaultSecret, cần RBAC và encryption at rest
Phù hợpVM, hybrid, không có K8sHệ 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ùng spring.application.name và 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, use spring.application.name and 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 @RefreshScope hoặ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/refresh to reload the Environment, and beans that need new values must be @RefreshScope or 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/busrefresh trê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/busrefresh on 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


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.