الفصل 3: التكوين المركزي مع Spring Cloud Config
🎯 أهداف الفصل
- فهم الحاجة إلى التكوين المركزي
- تكوين Spring Cloud Config Server
- توصيل الخدمات كـ Config Clients
- استخدام Git كخلفية للتكوين
- تنفيذ التحديث للتكوين في الوقت الفعلي
- إدارة التشفير للأسرار
1. لماذا التكوين المركزي؟
1.1 المشكلة بدون Config Server
microservices/
├── user-service/
│ └── application.yml # تكوين خاص
├── product-service/
│ └── application.yml # تكوين خاص
├── order-service/
│ └── application.yml # تكوين خاص
├── payment-service/
│ └── application.yml # تكوين خاص
└── inventory-service/
└── application.yml # تكوين خاص
مشاكل:
- تغيير كلمة مرور DB = تعديل 5 ملفات
- إعادة نشر 5 خدمات
- لا يوجد إصدار مركزي
- الأسرار مكشوفة في كل خدمة
- لا يوجد تحديث بدون إعادة تشغيل
1.2 الحل: Config Server
فوائد:
- مصدر واحد للحقيقة
- إصدار مع Git (تاريخ، فروع)
- تحديث بدون إعادة نشر
- تشفير الأسرار
- تكوين حسب البيئة (dev، prod)
2. بنية Spring Cloud Config
2.1 المكونات
2.2 تدفق التكوين
3. تكوين Config Server
3.1 هيكل المشروع
config-server/
├── pom.xml
└── src/main/
├── java/com/example/configserver/
│ └── ConfigServerApplication.java
└── resources/
└── application.yml
3.2 تبعيات Maven
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
</parent>
<groupId>com.example</groupId>
<artifactId>config-server</artifactId>
<version>1.0.0</version>
<properties>
<java.version>17</java.version>
<spring-cloud.version>2023.0.0</spring-cloud.version>
</properties>
<dependencies>
<!-- Config Server -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-server</artifactId>
</dependency>
<!-- Eureka Client (للتسجيل) -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!-- Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- Actuator -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
3.3 التطبيق الرئيسي
package com.example.configserver;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.config.server.EnableConfigServer;
@SpringBootApplication
@EnableConfigServer // تفعيل Config Server
public class ConfigServerApplication {
public static void main(String[] args) {
SpringApplication.run(ConfigServerApplication.class, args);
}
}
3.4 التكوين مع Git
# application.yml
server:
port: 8888
spring:
application:
name: config-server
cloud:
config:
server:
git:
# مستودع Git (GitHub، GitLab، Bitbucket، محلي)
uri: https://github.com/votre-org/config-repo
# الفرع الافتراضي
default-label: main
# المجلد في المستودع (اختياري)
search-paths: config
# الاستنساخ عند البدء
clone-on-start: true
# بيانات الاعتماد إذا كان المستودع خاصًا
username: ${GIT_USERNAME:}
password: ${GIT_PASSWORD:}
# المهلة
timeout: 10
# Eureka Client
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
# Security
spring:
security:
user:
name: config
password: config123
3.5 التكوين المحلي (للتطوير)
spring:
cloud:
config:
server:
native:
search-locations:
- file:///C:/config-repo
- classpath:/config
profiles:
active: native # استخدام نظام الملفات بدلاً من Git
4. هيكل مستودع التكوين
4.1 اتفاقية التسمية
config-repo/
├── application.yml # تكوين مشترك لجميع الخدمات
├── application-dev.yml # تكوين dev لجميع الخدمات
├── application-prod.yml # تكوين prod لجميع الخدمات
├── user-service.yml # تكوين خاص بخدمة المستخدم
├── user-service-dev.yml # خدمة المستخدم في dev
├── user-service-prod.yml # خدمة المستخدم في prod
├── product-service.yml
├── product-service-dev.yml
├── product-service-prod.yml
├── order-service.yml
├── order-service-dev.yml
└── order-service-prod.yml
4.2 أولوية التكوين
يتم دمج التكوين بهذا الترتيب (من الأقل إلى الأكثر أولوية):
application.yml(قاعدة مشتركة)application-\{profile\}.yml(ملف تعريف مشترك){service-name}.yml(خدمة محددة){service-name}-\{profile\}.yml(خدمة + ملف تعريف)
4.3 مثال: application.yml (مشترك)
# config-repo/application.yml
# تكوين مشترك لجميع الخدمات
# Eureka مشترك
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
instance:
prefer-ip-address: true
# Actuator مشترك
management:
endpoints:
web:
exposure:
include: health,info,refresh
endpoint:
health:
show-details: always
# تسجيل مشترك
logging:
level:
root: INFO
com.example: DEBUG
4.4 مثال: user-service.yml
# config-repo/user-service.yml
# تكوين خاص بخدمة المستخدم
server:
port: 8081
spring:
datasource:
url: jdbc:postgresql://localhost:5432/users_db
username: ${DB_USERNAME:postgres}
password: ${DB_PASSWORD:postgres}
jpa:
hibernate:
ddl-auto: validate
show-sql: false
# تكوين الأعمال
user:
default-role: USER
password-min-length: 8
token-expiration-hours: 24
4.5 مثال: user-service-dev.yml
# config-repo/user-service-dev.yml
# تجاوز لبيئة التطوير
spring:
datasource:
url: jdbc:h2:mem:users_dev
driver-class-name: org.h2.Driver
h2:
console:
enabled: true
jpa:
hibernate:
ddl-auto: create-drop
show-sql: true
logging:
level:
com.example.userservice: TRACE
org.hibernate.SQL: DEBUG
4.6 مثال: user-service-prod.yml
# config-repo/user-service-prod.yml
# تكوين الإنتاج
spring:
datasource:
url: jdbc:postgresql://prod-db.example.com:5432/users_db
username: ${DB_USERNAME}
password: ${DB_PASSWORD}
hikari:
maximum-pool-size: 20
minimum-idle: 5
jpa:
hibernate:
ddl-auto: none
show-sql: false
logging:
level:
root: WARN
com.example: INFO
# تعطيل النقاط الحساسة
management:
endpoints:
web:
exposure:
include: health,info
5. تكوين Config Client
5.1 تبعيات Maven (الخدمة)
<dependencies>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Config Client -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
<!-- Bootstrap (لتحميل التكوين قبل التطبيق) -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
<!-- Eureka Client -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!-- Actuator (لـ /refresh) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>
5.2 تكوين العميل (الطريقة 1: bootstrap.yml)
# src/main/resources/bootstrap.yml
# يتم تحميله قبل application.yml
spring:
application:
name: user-service # يجب أن يتطابق مع أسماء ملفات التكوين
profiles:
active: dev
cloud:
config:
# عنوان URL المباشر لـ Config Server
uri: http://localhost:8888
# بيانات الاعتماد إذا كان Config Server مؤمنًا
username: config
password: config123
# إعادة المحاولة إذا كان Config Server غير متاح
fail-fast: true
retry:
initial-interval: 1000
max-attempts: 6
max-interval: 2000
multiplier: 1.1
5.3 تكوين العميل (الطريقة 2: application.yml مع import)
# src/main/resources/application.yml
# Spring Boot 2.4+ - لا حاجة لـ bootstrap.yml
spring:
application:
name: user-service
profiles:
active: dev
config:
import: "optional:configserver:http://localhost:8888"
cloud:
config:
username: config
password: config123
5.4 الاكتشاف عبر Eureka
# بدلاً من عنوان URL ثابت، الاكتشاف عبر Eureka
spring:
cloud:
config:
discovery:
enabled: true
service-id: config-server # الاسم في Eureka
fail-fast: true
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
6. الوصول إلى الخصائص في الكود
6.1 الحقن مع @Value
@RestController
@RequestMapping("/api/users")
public class UserController {
@Value("${user.default-role}")
private String defaultRole;
@Value("${user.password-min-length}")
private int passwordMinLength;
@Value("${user.token-expiration-hours}")
private int tokenExpirationHours;
@GetMapping("/config")
public Map<String, Object> getConfig() {
return Map.of(
"defaultRole", defaultRole,
"passwordMinLength", passwordMinLength,
"tokenExpirationHours", tokenExpirationHours
);
}
}
6.2 الحقن مع @ConfigurationProperties
package com.example.userservice.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Component
@ConfigurationProperties(prefix = "user")
public class UserProperties {
private String defaultRole;
private int passwordMinLength;
private int tokenExpirationHours;
// Getters و Setters
public String getDefaultRole() { return defaultRole; }
public void setDefaultRole(String defaultRole) {
this.defaultRole = defaultRole;
}
public int getPasswordMinLength() { return passwordMinLength; }
public void setPasswordMinLength(int passwordMinLength) {
this.passwordMinLength = passwordMinLength;
}
public int getTokenExpirationHours() { return tokenExpirationHours; }
public void setTokenExpirationHours(int tokenExpirationHours) {
this.tokenExpirationHours = tokenExpirationHours;
}
}
@Service
public class UserService {
private final UserProperties userProperties;
public UserService(UserProperties userProperties) {
this.userProperties = userProperties;
}
public User createUser(UserRequest request) {
User user = new User();
user.setRole(userProperties.getDefaultRole());
// ...
return user;
}
}
7. تحديث التكوين في الوقت الفعلي
7.1 المشكلة
1. تعديل user-service-dev.yml في Git
2. Commit & Push
3. Config Server يرى التغيير
4. لكن: user-service لا يزال لديه التكوين القديم!
(لقد قام بتحميله عند البدء)
7.2 الحل 1: Endpoint /actuator/refresh
# كشف endpoint التحديث
management:
endpoints:
web:
exposure:
include: health,info,refresh
# تفعيل التحديث يدويًا
curl -X POST http://localhost:8081/actuator/refresh
7.3 Beans @RefreshScope
@RestController
@RequestMapping("/api/config")
@RefreshScope // سيتم إعادة إنشاء هذا bean عند التحديث
public class ConfigController {
@Value("${user.token-expiration-hours}")
private int tokenExpirationHours;
@GetMapping("/token-expiration")
public int getTokenExpiration() {
return tokenExpirationHours;
}
}
7.4 الحل 2: Spring Cloud Bus (التحديث التلقائي)
التبعية:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>
تفعيل التحديث العالمي:
curl -X POST http://config-server:8888/actuator/busrefresh
8. تشفير الأسرار
8.1 لماذا التشفير؟
# ❌ خطر: كلمة المرور مكشوفة في Git!
spring:
datasource:
password: SuperSecret123!
8.2 تكوين التشفير
# Config Server - application.yml
encrypt:
key: ${ENCRYPT_KEY:my-secret-encryption-key-32chars!}
أو مع keystore:
encrypt:
key-store:
location: classpath:/server.jks
password: keystorepassword
alias: configserver
secret: keypassword
8.3 تشفير قيمة
# تشفير
curl -X POST http://localhost:8888/encrypt -d "SuperSecret123!"
# النتيجة: AQB...encodedValue...
# فك التشفير (للتحقق)
curl -X POST http://localhost:8888/decrypt -d "AQB...encodedValue..."
# النتيجة: SuperSecret123!
8.4 استخدام القيمة المشفرة
# config-repo/user-service-prod.yml
spring:
datasource:
password: '{cipher}AQB...encodedValue...'
يقوم Config Server بفك التشفير تلقائيًا قبل الإرسال إلى العميل.
8.5 التشفير غير المتماثل (RSA)
# إنشاء keystore
keytool -genkeypair -alias configserver -keyalg RSA \
-dname "CN=Config Server,OU=Unit,O=Organization,L=City,S=State,C=US" \
-keypass changeme -keystore server.jks -storepass changeme
encrypt:
key-store:
location: file:/etc/config-server/server.jks
password: ${KEYSTORE_PASSWORD}
alias: configserver
secret: ${KEY_PASSWORD}
9. نقاط نهاية Config Server
9.1 أنماط URL
/{application}/{profile}[/{label}]
/{application}-{profile}.yml
/{label}/{application}-{profile}.yml
/{application}-{profile}.properties
/{label}/{application}-{profile}.properties
9.2 أمثلة
# تكوين user-service مع ملف تعريف dev
curl http://localhost:8888/user-service/dev
curl http://localhost:8888/user-service-dev.yml
# تكوين مع فرع محدد
curl http://localhost:8888/user-service/dev/feature-branch
# تكوين بتنسيق properties
curl http://localhost:8888/user-service-dev.properties
9.3 استجابة JSON
curl http://localhost:8888/user-service/dev
{
"name": "user-service",
"profiles": ["dev"],
"label": "main",
"version": "abc123def456",
"propertySources": [
{
"name": "https://github.com/org/config-repo/user-service-dev.yml",
"source": {
"spring.datasource.url": "jdbc:h2:mem:users_dev",
"user.default-role": "USER"
}
},
{
"name": "https://github.com/org/config-repo/user-service.yml",
"source": {
"server.port": 8081
}
},
{
"name": "https://github.com/org/config-repo/application.yml",
"source": {
"eureka.client.service-url.defaultZone": "http://localhost:8761/eureka/"
}
}
]
}
10. أفضل الممارسات
10.1 فصل البيئات
config-repo/
├── application.yml # لا أسرار هنا أبدًا
├── application-dev.yml # تكوين dev (يمكن أن يحتوي على قيم افتراضية)
├── application-staging.yml # تكوين staging
├── application-prod.yml # فقط مراجع لمتغيرات البيئة
10.2 الأسرار في الإنتاج
# ❌ سيء
spring:
datasource:
password: '{cipher}AQB...' # حتى وإن كان مشفرًا، فهو في Git
# ✅ جيد
spring:
datasource:
password: ${DB_PASSWORD} # متغير بيئة
10.3 فشل سريع
spring:
cloud:
config:
fail-fast: true # الخدمة لا تبدأ إذا كان Config Server غير متاح
retry:
max-attempts: 6
initial-interval: 1000
10.4 مهلات مناسبة
spring:
cloud:
config:
request-connect-timeout: 5000
request-read-timeout: 10000
10.5 فحص الصحة
// فحص الصحة يتضمن تلقائيًا Config Server
// GET /actuator/health
{
"status": "UP",
"components": {
"configServer": {
"status": "UP",
"details": {
"propertySources": [...]
}
}
}
}
11. ملخص
النقاط الرئيسية
✅ Config Server = مصدر واحد للتكوين
✅ Git Backend = إصدار، تاريخ، فروع
✅ اتفاقية التسمية = {service}-\{profile\}.yml
✅ @RefreshScope = Beans التي تعيد التحميل
✅ التشفير = أسرار مؤمنة
✅ Spring Cloud Bus = تحديث تلقائي موزع
ترتيب البدء
- Eureka Server (8761) - اكتشاف الخدمة
- Config Server (8888) - التكوين
- خدمات أخرى - تسترجع تكوينها ثم تسجل نفسها
قائمة التحقق
- Config Server تم تكوينه مع Git
- مستودع التكوين منظم
- العملاء تم تكوينهم مع bootstrap.yml أو spring.config.import
- Endpoint /refresh مكشوف
- الأسرار مشفرة أو عبر متغيرات البيئة
- الفشل السريع مفعل