➜ Old React website
Chung Cheuk Hang MichaelJava Web Developer
cURL 基本操作Remote debug Java 程式

Spring Cloud Config——使用獨立既 Git 配置 repo

Table of contents

1 Spring Cloud Config 簡介

Spring Cloud Config 既出現係為左針對喺雲端上運行既 apps 實現 config externalization,即係將 app 既配置綜合存放喺一個地方,例如係一個只負責存放 app 配置檔案既 Git repo,然後用一個 config server 去讀取呢個 Git repo 裡面既 app 配置檔案,當 app 開始運行,佢第一件會做既事就係從 config server 下載 app 配置,然後再用呢啲配置去初始化,例如係連接數據庫。

1.1 Externalize 配置

將配置檔案存放喺另一個 Git repo 既好處:
  • 配置係會因時間而改變,我地冇理由只係因為配置檔有改動而重新 build 過個 artifact。
    • 視乎 CI pipeline(如 Jenkins)既步驟數量以及複雜程度,所需時間可以好長。
      • 有啲大公司會喺 pipeline 加入 code scan plugins,例如 SonarQube、Snyk、Fortify,呢啲 code scan 工具都需要唔少時間,亦有可能會因為呢啲 code scan 工具既資料庫更新,突然多左一啲 vulnerabilities 而令 pipeline 執行失敗。
      • 如果 pipeline 係 deploy 上 public cloud,視乎雲端平台、雲端技術以及雲端平台既硬件資源,部署所需時間可能需要幾分鐘,慢過喺本地用 JVM 運行個 app。
  • 如果我地將配置存放喺另一個地方,咁我地只需要重新啟動個 app,或者 call Spring Boot Actuator 提供既 API,就可以刷新 app 既配置,方便快捷。
參考資料:

1.2 關於 12-Factor 既 Config

12-Factor App 係為現代既 cloud apps 而訂立既開發規範。有人認為 Spring Cloud Config 達到 12-Factor App 既其中一項規範——Config。
12 Factor implies that you should not store your configuration in a file in your application.
But you can source those configs using other tools
  • Puppet
  • Kubernetes ConfigMaps
  • Spring's configuration server
  • Databases/Data
Why bother? It reduces the likelihood of developers accidentally committing/sharing secret credentials. You ensure that your application/service does not have logic switching based on the environment.
參考資料:

2 設定 Git 配置 repo

2.1 建立 GitHub repo

先建立一個全新既 GitHub repo,內容如下:
  • /spring-cloud-config-client-demo
    • spring-cloud-config-client-demo-dev.yml
spring-cloud-config-client-demo-dev.yml
my: prop: Hello GitHub!

2.2 使用 SSH

ssh-keyscan -t ecdsa github.com ssh-keygen -t ecdsa -b 256 -m PEM -f keyfile
我地要將條 SSH public key(keyfile.pub)加落個 Git repo 度,而條 SSH private key(keyfile)就要放喺 config server 既配置度。

2.3 Clone Git repo

:: Windows Command Prompt SET "GIT_SSH_COMMAND=ssh -i keyfile -o IdentitiesOnly=yes" git clone ssh://git@github.com/blackr1234/spring-cloud-config-demo.git

3 動手寫

我地會用到 Spring Cloud Dependencies 黎做 dependency management,而 Spring Cloud Config Server 又會引入 Spring Boot Starter Web 既 dependency。

3.1 Config server

Project structure:
  • src/main/java
    • /code
      • MainConfigServerApp.java
  • src/main/resources
    • application.yml
    • bootstrap.yml

3.1.1 Maven dependencies

1<dependencyManagement> 2 <dependencies> 3 <dependency> 4 <groupId>org.springframework.cloud</groupId> 5 <artifactId>spring-cloud-dependencies</artifactId> 6 <version>Hoxton.SR12</version> 7 <type>pom</type> 8 <scope>import</scope> 9 </dependency> 10 <dependency> 11 <groupId>org.springframework.boot</groupId> 12 <artifactId>spring-boot-dependencies</artifactId> 13 <version>2.3.12.RELEASE</version> 14 <type>pom</type> 15 <scope>import</scope> 16 </dependency> 17 </dependencies> 18</dependencyManagement> 19 20<dependencies> 21 <dependency> 22 <groupId>org.springframework.cloud</groupId> 23 <artifactId>spring-cloud-config-server</artifactId> 24 </dependency> 25</dependencies>

3.1.2 Java code

MainConfigServerApp.java
1@EnableConfigServer 2@SpringBootApplication 3public class MainConfigServerApp { 4 5 public static void main(String[] args) { 6 SpringApplication.run(MainConfigServerApp.class, args); 7 } 8}

3.1.3 Bootstrap 配置

bootstrap.yml
1encrypt: 2 key: michael ## or use the ENCRYPT_KEY environment variable 3 4spring: 5 cloud: 6 config: 7 server: 8 encrypt: 9 enabled: true

3.1.4 Application 配置

application.yml
1server: 2 port: 8889 3 4spring: 5 cloud: 6 config: 7 server: 8 git: 9 clone-on-start: true 10 refresh-rate: 10 11 ignore-local-ssh-settings: true 12 uri: git@github.com:blackr1234/spring-cloud-config-demo.git 13 default-label: master 14 search-paths: 15 - "{application}" 16 host-key: 'AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEmKSENjQEezOmxkZMy7opKgwFB9nkt5YRrYMjNuG5N87uRgg6CLrbo5wAdT/y6v0mKV0U2w0WZ2YB/++Tpockg=' 17 host-key-algorithm: ecdsa-sha2-nistp256 18 private-key: | 19 -----BEGIN EC PRIVATE KEY----- 20 MHcCAQEEIPnupl8oxl0Wj6xfOd/PobBG48m3pVkmubPem1XSyexEoAoGCCqGSM49 21 AwEHoUQDQgAEUGO+DDpbpsgp3C+H68iTTaklmcnk2MSbbh4bwQVnMws09eqFqvA4 22 RzcTRtAXt2IWkq4JHUg6rtjDnc/0zwSQyQ== 23 -----END EC PRIVATE KEY-----
註:
  • spring.cloud.config.server.git.host-key 係 GitHub 既 host key,除非改用 GitLab 或者用其他 host key algorithm。
  • spring.cloud.config.server.git.host-key-algorithm 要對應返我地喺 2.2 使用 SSH 生成 key 所使用既 command。
  • spring.cloud.config.server.git.private-key 係我地喺 2.2 使用 SSH 生成既 keyfile 既內容。

3.2 Config client

Project structure:
  • src/main/java
    • /code
      • MainConfigClientApp.java
      • MyComponent.java
  • src/main/resources
    • application.yml
    • bootstrap.yml

3.2.1 Maven dependencies

1<dependencyManagement> 2 <dependencies> 3 <dependency> 4 <groupId>org.springframework.cloud</groupId> 5 <artifactId>spring-cloud-dependencies</artifactId> 6 <version>Hoxton.SR12</version> 7 <type>pom</type> 8 <scope>import</scope> 9 </dependency> 10 <dependency> 11 <groupId>org.springframework.boot</groupId> 12 <artifactId>spring-boot-dependencies</artifactId> 13 <version>2.3.12.RELEASE</version> 14 <type>pom</type> 15 <scope>import</scope> 16 </dependency> 17 </dependencies> 18</dependencyManagement> 19 20<dependencies> 21 <dependency> 22 <groupId>org.springframework.cloud</groupId> 23 <artifactId>spring-cloud-starter-config</artifactId> 24 </dependency> 25 <dependency> 26 <groupId>org.springframework.boot</groupId> 27 <artifactId>spring-boot-starter-web</artifactId> 28 </dependency> 29 <dependency> 30 <groupId>org.springframework.boot</groupId> 31 <artifactId>spring-boot-starter-actuator</artifactId> 32 </dependency> 33</dependencies>

3.2.2 Java code

MainConfigClientApp.java
1@SpringBootApplication 2public class MainConfigClientApp { 3 4 public static void main(String[] args) { 5 SpringApplication.run(MainConfigClientApp.class, args); 6 } 7}
MyComponent.java
1@Slf4j 2@RefreshScope 3@Component 4public class MyComponent { 5 6 @Value("${my.prop:not-found}") 7 private String myProp; 8 9 @PostConstruct 10 public void init() { 11 log.info("my.prop: {}", myProp); 12 } 13 14 @EventListener 15 public void refresh(RefreshScopeRefreshedEvent event) { 16 log.info("Refreshed class [{}].", getClass().getName()); 17 } 18}

3.2.3 Bootstrap 配置

bootstrap.yml
1spring: 2 application: 3 name: spring-cloud-config-client-demo 4 profiles: 5 active: dev ## should be externalized 6 cloud: 7 config: 8 enabled: true 9 uri: http://localhost:8889

3.2.4 Application 配置

application.yml
management: endpoints: web: exposure: include: refresh, env

4 簡單測試

4.1 步驟

  1. 啟動 config server。
  2. 啟動 config client。
  3. 檢查 my.prop 既 value,應該係而家 Git 上面既 value。
  4. 更改 Git 上既 my.prop 既 value。
  5. Call config client 既 /actuator/refresh API。
    • Response body 裡面會有一個 array,其中一個 string 就係 my.prop,意味住 my.prop 既 value 改變左。
  6. 檢查 my.prop 既 value,應該係我地改成既新 value。

4.2 檢查 property value

針對 my.prop,我地有以下既方法:
  1. Call config client 既 /actuator/env API,response body 會有 my.prop 既 value,呢個方法只需要配置好 Actuator 就可以用。
  2. 因為我地有特登寫 code 去做 logging,所以我地可以留意 config client 既 log 裡面 my.prop 既 value。

5 配置檔既應用次序

喺 Spring,如果我地唔用 Spring Cloud Config 去 externalize 配置,咁我地個 artifact JAR 檔裡面可以有:
  1. application
  2. application-{profile}
因為 Spring 支援 YAML 以及 Java properties 既格式,而 YAML 格式又可以用 .yaml 以及 .yml 作為副檔名,咁我地其實最盡可以有 6 個檔案。
而如果我地用 Spring Cloud Config 去 externalize 配置,咁我地喺既 Git repo 就會有:
  1. application
  2. {spring.application.name}
  3. application-{profile}
  4. {spring.application.name}-{profile}
如果又各有 yaml.yml 以及 .properties 副檔名,咁就可以有 12 個組合咁多既檔案。
經過反翻既測試,最後搵到 Spring Cloud Config 既 Git backend 讀取檔案既次序係:
  1. application.yaml
  2. application.yml
  3. application.properties
  4. {spring.application.name}.yaml
  5. {spring.application.name}.yml
  6. {spring.application.name}.properties
  7. application-{profile}.yaml
  8. application-{profile}.yml
  9. application-{profile}.properties
  10. {spring.application.name}-{profile}.yaml
  11. {spring.application.name}-{profile}.yml
  12. {spring.application.name}-{profile}.properties
解釋:
  • 呢個次序既意思係,當同一個名既配置(例如 my.prop)出現喺以上所有既檔案裡面,咁 Spring Cloud Config 係會以呢個次序去讀取檔案去取值,而較遲讀取既值係會覆蓋之前讀取既值。
  • Java properties 檔會「贏」過曬 YAML 格式,最終值會係黎自 Java properties 配置檔。
  • -{profile} 既檔案就係 default profile,而特定既環境 profile 係一定會比較遲讀取,覆蓋默認既配置。

6 新版本

6.1 App 需要修改既地方

6.1.1 版本

更新 config server 以及 config client 既 dependency managements 去以下既版本:
1<dependencyManagement> 2 <dependencies> 3 <dependency> 4 <groupId>org.springframework.boot</groupId> 5 <artifactId>spring-boot-dependencies</artifactId> 6 <version>2.7.0</version> 7 <type>pom</type> 8 <scope>import</scope> 9 </dependency> 10 <dependency> 11 <groupId>org.springframework.cloud</groupId> 12 <artifactId>spring-cloud-dependencies</artifactId> 13 <version>2021.0.3</version> 14 <type>pom</type> 15 <scope>import</scope> 16 </dependency> 17 </dependencies> 18</dependencyManagement>

6.1.2 bootstrap.yml 合併入 application.yml

最新版既 Spring Cloud Config 已經唔再用 bootstrap.yml 檔,所以我地要將佢既 properties 搬去 application.yml
不過如果真係想用返 bootstrap.yml 呢個舊既方式,可以加入呢個 dependency:
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-config</artifactId> </dependency>

6.1.3 新 spring.config.import 配置

Spring Cloud Config client 既 spring.cloud.config.uri 已經被 spring.config.import 取代,而 value 都有所唔同:
1# 舊配置,bootstrap.yml 檔 2spring: 3 cloud: 4 config: 5 uri: http://localhost:8889 6 7--- 8 9# 新配置,application.yml 檔 10spring: 11 config: 12 import: 'optional:configserver:http://localhost:8889'
註:
  • 如果唔加上 optional:,咁當 config client 啟動既時候連接唔到 config server,config client 就會 throw exception 並且終止運行。
  • 如果加上 optional:
    1. 就算 config client 啟動果一刻連接唔到 config server,佢都唔會終止運行。
    2. 如果之後我地手動 call config client 既 /actuator/refresh API,而 config client 成功連接到 config server,咁佢會重新獲取配置,而有 @RefreshScope 既 components 都會獲得新既配置數值。

7 連線容錯測試

根據測試,Git backend 係可以容許連線失敗,因為 config server 會有 file cache。

7.1 使用 Git backend 而 Git 連線失敗

模擬連線失敗場景:
  1. 啟動 config server。
  2. 啟動 config client。
  3. 呢個時候,我地可以見到 config server 會將個 Git repo clone 到一個叫 config-repo-<random numbers> 既 folder,呢個 folder 係喺我地既 JVM temp folder(e.g. C:/Users/Michael/AppData/Local/Temp)裡面。
  4. 喺 OS 層面斷開網絡。
  5. 重新啟動 config client,佢依然可以喺 config server 度拎到配置,咁係因為 config server 用左上面提到既 file cache。

8 參考資料

延伸閱讀: