99 Ngày Spring — Ngày 58: Spring Boot Actuator
Sau bảy ngày test, hôm nay là câu hỏi tiếp theo của một service sắp lên production: làm sao biết nó đang sống, đang chạy bản nào, và đang chịu tải ra sao — mà không SSH vào máy chủ. Câu trả lời của Spring Boot là Actuator: một starter, và ứng dụng có ngay /actuator/health, /info, /metrics. Nhưng cùng starter đó cũng có env, heapdump, shutdown — nên nửa bài hôm nay là lộ ra cái gì, và khóa thế nào.

Actuator là gì — và mặc định lộ ra những gì
Actuator là bộ endpoint quản trị Boot gắn sẵn: mỗi endpoint là một cửa sổ nhìn vào ứng dụng đang chạy. Điểm đầu tiên cần nhớ: Boot 3 mặc định chỉ lộ đúng một endpoint qua web — health. Muốn thêm gì phải liệt kê đích danh trong management.endpoints.web.exposure.include; dấu * tiện tay ở dev là cách nhanh nhất để đưa env và heapdump lên production.
# ACTUATOR — "bảng đồng hồ" của ứng dụng: sức khỏe, cấu hình, số đo — qua HTTP.
# ① Thêm một starter là có ngay:
# implementation 'org.springframework.boot:spring-boot-starter-actuator' (Gradle)
# <artifactId>spring-boot-starter-actuator</artifactId> (Maven)
# GET /actuator → danh sách endpoint đang lộ ra (HAL links)
# GET /actuator/health → {"status":"UP"}
# ② MẶC ĐỊNH BOOT 3 CHỈ LỘ RA ĐÚNG MỘT endpoint qua web: health.
# (JMX lộ nhiều hơn, nhưng JMX đã TẮT mặc định từ Boot 2.2.)
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus # ⭐ liệt kê ĐÍCH DANH thứ bạn cần
# include: "*" # ❌ lộ hết — kể cả env, heapdump, threaddump
base-path: /manage # đổi /actuator nếu muốn — KHÔNG phải bảo mật
# ③ BẢN ĐỒ ENDPOINT — biết cái nào NGUY HIỂM trước khi bật:
# health sức khỏe tổng hợp: DB, disk, broker... ✅ probe cho k8s / load balancer
# info phiên bản, commit git, thời điểm build ✅ "đang chạy bản nào?"
# metrics số đo Micrometer: request, JVM, pool kết nối ✅ + prometheus để scrape
# loggers xem VÀ ĐỔI log level lúc chạy (POST) ⚠ hữu ích, phải bảo vệ
# env toàn bộ biến môi trường + property ❌ mật khẩu, token, key
# configprops mọi @ConfigurationProperties đã bind ❌ như env
# beans, mappings, conditions nội thất ứng dụng ⚠ chỉ cho dev
# heapdump tải cả heap về máy — chứa MỌI thứ trong bộ nhớ ❌ 💥 dữ liệu khách hàng
# threaddump stack của mọi luồng ❌ lộ cấu trúc, tốn CPU
# shutdown tắt ứng dụng qua HTTP — TẮT mặc định ❌ không bật trên production
# ④ Từng endpoint còn có công tắc riêng, độc lập với exposure:
management:
endpoint:
shutdown:
enabled: false # mặc định đã false — đừng đổi
health:
show-details: when_authorized # never | when_authorized | always
roles: OPS # chi tiết (db, disk...) chỉ cho người có ROLE_OPS
# ⭐ BA LỚP: enabled (endpoint có tồn tại không) → exposure (có lộ qua web không)
# → security (ai được gọi — Ngày 42). Thiếu lớp cuối thì hai lớp đầu chỉ là màn che.Mặc định an toàn: chỉ
healthlộ qua web, JMX tắt từ Boot 2.2,shutdowntắt hẳn — đừng phá mặc định bằnginclude: "*".Chia endpoint thành ba nhóm: bật được (
health,info,metrics,prometheus), bật có bảo vệ (loggers,beans,mappings), không bật (env,configprops,heapdump,threaddump,shutdown).Đổi
base-pathkhông phải bảo mật — kẻ dò sẽ tìm thấy; ba lớp thật làenabled→exposure→ security.
health, info, metrics — ba endpoint đáng giá nhất
health gom các HealthIndicator — Boot tự tạo cho DB, disk, Redis, Kafka đã cấu hình, bạn viết thêm cho thứ ứng dụng không sống nổi nếu thiếu. Nhưng một component DOWN kéo cả status thành 503, nên Kubernetes cần hai cái nhìn khác nhau: liveness ("JVM còn ổn không" — DB chết không được làm nó DOWN, vì restart pod không sửa được DB) và readiness ("nhận request được chưa"). info trả lời "đang chạy bản nào" từ build và git; metrics là Micrometer — request, JVM, pool, và số đo nghiệp vụ bạn tự thêm.
// HEALTH, INFO, METRICS — ba endpoint đáng bật nhất, và cách làm cho chúng NÓI ĐÚNG.
// ① HEALTH — Boot tự thêm HealthIndicator cho DB, disk, Redis, Kafka... đã cấu hình.
// Tự viết một cái cho thứ ứng dụng KHÔNG SỐNG NỔI nếu thiếu — ví dụ cổng thanh toán:
@Component
public class CongThanhToanHealth implements HealthIndicator {
private final CongThanhToanClient client;
@Override
public Health health() {
try {
var ping = client.ping(); // ⭐ rẻ, có timeout ngắn
return Health.up().withDetail("doTreMs", ping.ms()).build();
} catch (Exception e) {
return Health.down(e).withDetail("endpoint", client.url()).build();
}
}
}
// GET /actuator/health → {"status":"DOWN","components":{"congThanhToan":{"status":"DOWN",...}}}
// ❗ Một component DOWN kéo cả status tổng thành DOWN — HTTP 503 — và load balancer sẽ rút
// instance ra. Thứ KHÔNG bắt buộc (email, thống kê) thì đừng cho nó quyết định sống chết:
// trả Health.unknown(), hoặc để nó ngoài group readiness.
// ② HEALTH GROUP + PROBE KUBERNETES — "còn sống" khác "sẵn sàng nhận request":
// application.yml:
// management.endpoint.health.probes.enabled: true # trên k8s Boot tự bật
// management.endpoint.health.group.readiness.include: readinessState,db,congThanhToan
// management.endpoint.health.group.liveness.include: livenessState
// GET /actuator/health/liveness → chỉ trạng thái nội bộ JVM. DB chết KHÔNG được làm nó DOWN,
// vì k8s sẽ RESTART pod — mà restart không sửa được DB.
// GET /actuator/health/readiness → DB, cổng thanh toán. DOWN thì k8s NGỪNG ĐƯA traffic tới,
// pod vẫn sống, chờ DB quay lại.
// ③ INFO — "server này đang chạy bản nào?":
// build.gradle: springBoot { buildInfo() } (Maven: goal build-info của spring-boot-maven-plugin)
// + plugin git-commit-id → /actuator/info: {"build":{"version":"1.4.2"},"git":{"commit":{"id":"3f9e1c"}}}
// Thêm gì tùy ý: management.info.env.enabled=true + info.doi=Thanh toán, hoặc một InfoContributor
// ④ METRICS — Micrometer đo sẵn HTTP, JVM, pool kết nối, cache; bạn thêm số đo NGHIỆP VỤ:
@Service
public class DonHangService {
private final Counter donTao;
public DonHangService(MeterRegistry reg) {
donTao = Counter.builder("donhang.tao") // ⭐ tên chấm.phân.cách, tag cho chiều
.tag("kenh", "web").register(reg);
}
@Timed(value = "donhang.thanhtoan", histogram = true) // thời gian + phân vị p95/p99
public void thanhToan(DonHang d) { /* ... */ donTao.increment(); }
}
// GET /actuator/metrics/http.server.requests?tag=uri:/api/don-hang&tag=status:500
// GET /actuator/prometheus ← cần micrometer-registry-prometheus; Prometheus scrape, Grafana vẽ
// ❗ Đừng tag bằng giá trị KHÔNG GIỚI HẠN (id khách, id đơn): mỗi giá trị là một chuỗi
// thời gian mới — "cardinality explosion" làm Prometheus chết trước ứng dụng của bạn.HealthIndicatortự viết phải rẻ và có timeout — nó bị gọi mỗi vài giây bởi load balancer; thứ không bắt buộc thìHealth.unknown()hoặc để ngoài groupreadiness.Liveness ≠ readiness: DB chết → readiness DOWN (ngừng nhận traffic, pod sống chờ), liveness vẫn UP; đưa DB vào liveness là tự tạo vòng lặp restart.
infocóbuildvàgitkhi bậtbuildInfo()và plugingit-commit-id— câu "deploy bản nào rồi?" trả lời bằng mộtcurl.Số đo nghiệp vụ qua
MeterRegistryvà@Timed; không tag bằng giá trị vô hạn (id khách, id đơn) — cardinality explosion giết Prometheus.
Expose an toàn — cổng riêng, quyền riêng, bật ít thôi
Actuator không phải lỗ hổng, actuator lộ ra không kiểm soát mới là lỗ hổng — và heapdump công khai là vụ rò rỉ kinh điển. Ba lớp khóa, dùng cả ba: cổng quản trị riêng management.server.port chỉ mở trong mạng nội bộ; một SecurityFilterChain riêng với EndpointRequest của Ngày 42 — health permitAll cho probe, còn lại hasRole; và chỉ include thứ cần, exclude thẳng tay env, heapdump, threaddump.
// EXPOSE AN TOÀN — actuator là cửa hậu nếu bạn không khóa. Ba lớp khóa, dùng cả ba.
// ① LỚP 1: CỔNG RIÊNG — actuator KHÔNG đi chung cổng với API công khai.
// application.yml:
// server.port: 8080 # API — ra internet qua load balancer
// management.server.port: 9090 # actuator — CHỈ mạng nội bộ / Prometheus / k8s
// management.server.address: 127.0.0.1 # hoặc IP nội bộ; firewall / k8s không mở 9090 ra ngoài
// ➜ Kẻ tấn công không tới được cổng thì không cần bàn tới bước sau. Nhưng "mạng nội bộ"
// rất hay bị cấu hình sai — nên vẫn cần lớp 2.
// ② LỚP 2: SECURITY FILTER CHAIN riêng cho actuator (Ngày 42), ưu tiên cao hơn chain API:
@Configuration
public class ActuatorSecurityConfig {
@Bean @Order(1)
SecurityFilterChain actuatorChain(HttpSecurity http) throws Exception {
http.securityMatcher(EndpointRequest.toAnyEndpoint()) // ⭐ khớp MỌI endpoint actuator,
.authorizeHttpRequests(a -> a // kể cả khi đổi base-path / cổng
.requestMatchers(EndpointRequest.to(HealthEndpoint.class)).permitAll()
// probe k8s không có credential
.requestMatchers(EndpointRequest.to("prometheus")).hasRole("METRICS")
.anyRequest().hasRole("OPS")) // loggers, metrics, info...
.httpBasic(Customizer.withDefaults()) // Prometheus / ops dùng basic
.csrf(c -> c.ignoringRequestMatchers(EndpointRequest.toAnyEndpoint()));
return http.build(); // POST /loggers cần bỏ CSRF
}
}
// ❗ Boot 3 KHÔNG tự permitAll health khi có Spring Security — thiếu chain này thì
// /actuator/health cũng đòi đăng nhập, probe k8s fail → pod bị restart vòng lặp.
// ③ LỚP 3: CHỈ BẬT THỨ CẦN, và biết mỗi thứ lộ gì:
// management.endpoints.web.exposure.include: health,info,metrics,prometheus,loggers
// management.endpoints.web.exposure.exclude: env,heapdump,threaddump # ⭐ exclude THẮNG include
//
// env / configprops: Boot 3 che giá trị thành ****** MẶC ĐỊNH — hiện lại bằng
// management.endpoint.env.show-values: when_authorized (never | when_authorized | always)
// — và kể cả khi đó, đừng bật env trên production: một property đặt tên "quên" chữ
// password / secret / key vẫn có thể lộ nguyên vẹn.
// heapdump: một file .hprof chứa MỌI object trong bộ nhớ — token phiên, dữ liệu khách,
// khóa giải mã. Cần thì lấy bằng jcmd trên máy chủ, không qua HTTP.
// shutdown: management.endpoint.shutdown.enabled=false (mặc định) — đừng đổi trên production.
// loggers: POST /actuator/loggers/vn.motdev {"configuredLevel":"DEBUG"} đổi level KHÔNG restart
// — cực hữu ích khi săn bug production, và cũng là lý do nó phải nằm sau hasRole("OPS").
// ⭐ CHECKLIST TRƯỚC KHI DEPLOY:
// [ ] include liệt kê đích danh, không "*" [ ] cổng quản trị riêng, không mở ra internet
// [ ] health permitAll, còn lại có role [ ] show-details: when_authorized
// [ ] env / heapdump / threaddump không lộ [ ] curl -i https://api.cua.ban/actuator → 401 hoặc 404EndpointRequest.toAnyEndpoint()khớp mọi endpoint actuator dù đổi base-path hay cổng — đừng viết tay"/actuator/**".Có Spring Security thì Boot 3 không tự mở health — thiếu
permitAllchoHealthEndpointlà probe k8s nhận 401 và pod restart vòng lặp.excludethắnginclude— một dòngexclude: env,heapdump,threaddumpở profile production là lưới an toàn khi ai đó lỡ tayinclude: "*".loggersđổi log level không cần restart — giữ nó, nhưng sauhasRole("OPS")và bỏ CSRF cho actuator để POST được.
Bài tập nhỏ
Thêm
spring-boot-starter-actuator, gọiGET /actuator— đếm số endpoint lộ ra khi chưa cấu hình gì.Đặt
include: "*", mở/actuator/envvà tìm xemDATABASE_URLhiện thế nào; rồi đổi thành include đích danh.Viết một
HealthIndicatortrảDOWNkhi một file cấu hình thiếu — xem mã HTTP của/actuator/healthđổi thành 503.Cấu hình group
readinessgồmdb, gọi/actuator/health/livenessvà/readinesskhi tắt DB — cái nào DOWN?Viết chain
@Order(1)vớiEndpointRequest:health200 không cần đăng nhập,metrics401 khi không có credential, 200 với userROLE_OPS— ba test bằngMockMvc(Ngày 56).
Kết lại
Bốn ý gói lại hôm nay: Actuator là bảng đồng hồ của ứng dụng, và Boot 3 mặc định chỉ lộ health — mọi thứ thêm vào phải liệt kê đích danh; health gom các HealthIndicator và Kubernetes cần liveness tách khỏi readiness, info trả lời "bản nào", metrics là Micrometer với tag hữu hạn; expose an toàn là ba lớp khóa — cổng riêng, SecurityFilterChain riêng với EndpointRequest, và exclude thẳng env/heapdump/threaddump; cuối cùng, đổi base-path không phải bảo mật, permitAll cho health là bắt buộc khi có Spring Security. Ngày 59 quay lại chất lượng test với câu hỏi khó: coverage — JaCoCo đo gì, kim tự tháp test, và con số nào không nên chạy theo. Hẹn gặp lại!
Site Admin
Engineer and writer. Building things with TypeScript and distributed systems.
Bình luận (0)
Bạn cần đăng nhập bằng Google để bình luận.
Hãy là người bình luận đầu tiên.


