Spring Cloud Config

Aki régebb óta követi a blogomat, tudhatja, hogy az alkalmazások konfigurációjának kezelése, tárolása és betöltése mindig érdekelt. Mivel manapság Spring környezetben dolgozom, adta magát a Spring Cloud Config. A Pivotal (a Spring mögött álló cég) célja a Spring Boot és a Spring Cloud fejlesztésével az, hogy egy olyan egységes, pehelysúlyú, könnyen használható környezetet adjon a microservices architektúra és a cloud kihívásaira, melyet a Spring Frameworkkel adott a nagyvállalati Java fejlesztés megkönnyítésére, valós alternatívát nyújtva a Java EE helyett. A koncepció ugyanaz, főleg létező eszközök egységes keretbe foglalása, elosztott környezetben elterjedt minták alkalmazása.

A Spring Cloud Config használatával könnyen építhető olyan szerver alkalmazás, mely a konfigurációkat tárolja és szolgálja ki a különböző klienseknek. Ezek tárolása különböző lehet, jelenleg fájlrendszer és verziókezelő rendszer (Subversion, Git), és a klienseket REST interfészen szolgálja ki. Képes a jelszavakat és különböző érzékeny konfigurációkat szimmetrikus vagy aszimmetrikus kódolással tárolni. A megváltozott konfigurációról értesíteni tudja a klienseket. Felhasználói felületet nem biztosít a konfigurációk szerkesztésére, hiszen pl. a Git repository képes az állományok verziózott tárolására, hozzáférés szabályozására, ezen kívül rendkívül jó felületek is vannak hozzá (kezdve a parancssorral). A Config Server ezen kívül bármilyen Spring Boot alkalmazásba könnyen beágyazható. A Config Client egy könyvtár mely szintén Spring Boot alkalmazásokban használható a legegyszerűbben, és képes kapcsolódni a Config Server-hez, és a konfigurációkat onnan betölteni. A Spring Unified Property Managementjébe illeszkedik, használva az Environment és PropertySource absztrakciókat.

Ebben a posztban megmutatom, hogy lehet implementálni egy Spring Boot alkalmazást Config Clientként, hogyan kell felépíteni egy Config Servert. Látunk egy példát a titkosításra. Megnézzük, hogy a szerver képes értesíteni a klienseket Spring Cloud Bus infrastruktúrán keresztül (RabbitMQ-t használva). Sőt a végén a spring-boot-admin grafikus adminisztrációs interfészt is megnézzük Sping Boot alkalmazásokhoz.

A példa projekt megtalálható a GitHub-on, vicziani/jtechlog-config néven.

A kliensként induljunk ki egy üres Spring Boot alkalmazásból, melyet Spring Initializr webes szolgáltatással legenerálhatunk. Válasszuk ki a Web, Actuator és Config Client függőségeket. Látható, hogy a generált projekt szülő projektje a org.springframework.boot:spring-boot-starter-parent mely a Spring Boot verzió számát definiálja, függőségként szerepel a org.springframework.boot:spring-boot-starter-web, org.springframework.boot:spring-boot-starter-actuator és a org.springframework.cloud:spring-cloud-starter-config. A dependencyManagement részben pedig a org.springframework.cloud:spring-cloud-dependencies, mely a Spring Cloud függőségek verzió számait tartalmazza.

Hozzunk létre egy Controller osztályt a com.example csomagban a következő kóddal.

package com.example;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class MessageController {

    private String message;

    public MessageController(@Value("${message}") String message) {
        this.message = message;
    }

    @RequestMapping("/")
    public String hello() {
        return message;
    }
}

Majd az application.properties állományba vegyük fel a message property értékét:

message = Hello World!

A ClientApplication alkalmazást elindítva a böngészőben a http://localhost:8080 címen megjelenik az üzenet.

Ha figyelmesek vagyunk, a naplóban láthatjuk, hogy az alkalmazás már próbál a Config Serverhez csatlakozni a http://localhost:8888 címen.

2016-09-03 22:21:52.788  INFO 9624 --- [           main] c.c.c.ConfigServicePropertySourceLocator : Fetching config from server at: http://localhost:8888
2016-09-03 22:21:52.876  WARN 9624 --- [           main] c.c.c.ConfigServicePropertySourceLocator : Could not locate PropertySource: I/O error on GET request for "http://localhost:8888/application/default": Connection refused; nested exception is java.net.ConnectException: Connection refused

Természetesen a cím a spring.cloud.config.uri property-ben felülbírálható. Azonban itt felmerül, hogy ennek megadása az application.properties állományban nem lesz jó, hiszen pont az ebben szereplő értékeket akarjuk a szervertől elkérni. Ezért hozzunk létre egy bootstrap.properties állományt, és ebbe írjuk a megfelelő címet, ugyanis ez töltődik be az application context indulása előtt, un. bootstrap fázisban.

Következő lépésben hozzuk létre a szervert, szintén a Spring Initializr szolgáltatással. Most elég a Web és a Config server függőségeket kiválasztanunk. A ServerApplication osztályra tegyük rá a @EnableConfigServer annotációt, ezzel már be is ágyaztunk egy Config Servert. Majd be kell állítanunk az application.properties állományban, hogy a 8888 porton fusson, valamint a Git repository elérhetőségét, ahol a konfigurációs állományokat tárolni fogjuk.

server.port: 8888
spring.cloud.config.server.git.uri: file:///home/jtechlog/config

Hozzuk is létre ezt a könyvtárat, majd inicializáljunk egy Git repository-t, és hozzunk létre egy application.properties állományt, és commitoljuk be.

mkdir config
git init
cat > application.properties
message = Hello Git!
git add .
git commit -m "Initial commit"

Az alkalmazást elindítva elég sok címen ki tudja szolgálni a kéréseket, tipikusan három fő paraméter megadásával.

/{application}/{profile}[/{label}]
/{application}-{profile}.yml
/{label}/{application}-{profile}.yml
/{application}-{profile}.properties
/{label}/{application}-{profile}.properties

Ebből az application az alkalmazás neve, melynek értéke application, ha magunk nem definiáljuk. Felülírhatjuk a kliensben a bootstrap.properties állományban a spring.config.name property-vel. A második a profile, mellyel (vagy melyekkel) a kliens indítva lett. A label a Git repository-ra vonatkozik, mely lehet commit id, branch neve vagy tag név is. Ez utóbbi értéke alapból master. Ezek mindegyike felülírható spring.cloud.config kezdetű property-kkel is a kliens oldalon, pl. a label esetén a spring.cloud.config.label property-vel.

Amennyiben file típusú repository-t adunk meg, akkor azt direktben olvassa. Amennyiben azonban pl. http(s) vagy ssh protokollt használunk, a temp könyvtárba klónozza a repository-t. Alapértelmezetten a klónozás akkor történik, mikor az első kérés jön, de ezt felül lehet írni a cloneOnStart property-vel.

Beírva a http://localhost:8888/application/default címet, valami hasonlót kapunk:

{
    "name": "application",
    "profiles": [
        "default"
    ],
    "label": "master",
    "version": "37d958ed0ef1a89ca53af1978678e345cba9d149",
    "propertySources": [
        {
            "name": "file:///home/jtechlog/config/application.properties",
            "source": {
                "message": "Hello Git!"
            }
        }
    ]
}

Ha elindítjuk a kliens alkalmazást, máris tud kapcsolódni a szerverhez, és a konfigurációt már onnan tölti le, megjelenik a Hello Git! üzenet. A logban valami hasonló jelenik meg:

2016-09-04 15:35:41.827  INFO 6766 --- [           main] c.c.c.ConfigServicePropertySourceLocator : Fetching config from server at: http://localhost:8888
2016-09-04 15:35:42.136  INFO 6766 --- [           main] c.c.c.ConfigServicePropertySourceLocator : Located environment: name=application, profiles=[default], label=master, version=37d958ed0ef1a89ca53af1978678e345cba9d149
2016-09-04 15:35:42.136  INFO 6766 --- [           main] b.c.PropertySourceBootstrapConfiguration : Located property source: CompositePropertySource [name='configService', propertySources=[MapPropertySource [name='file:///home/jtechlog/config/application.properties']]]

A kliens alkalmazás healthcheckjét nézve (http://localhost:8080/health) is látszik, hogy él a szerverrel a kapcsolat:

"configServer": {
    "status": "UP",
    "propertySources": [
        "file:///home/jtechlog/config/application.properties"
    ]
}

Sőt, a környezeti változókat kiíratva (http://localhost:8080/env) is látható:

"configService:file:///home/jtechlog/config/application.properties": {

    "message": "Hello Git!"

},
"applicationConfig: [classpath:/application.properties]": {

    "message": "Hello World!"

},

A Config Server healthcheck szolgáltatása (http://localhost:8888/health) is tökéletesen mutatja, hogy milyen repository-kat ismer:

configServer": {
    "status": "UP",
    "repositories": [
        {
            "sources": [
                "file:///home/vicziani/Downloads/szakmainap/config/application.properties"
            ],
            "name": "app",
            "profiles": [
                "default"
            ],
            "label": "master"
        }
    ]
}

Amennyiben nem Spring Boot klienst használunk, a Config Server képes properties, és yaml formátumokban is kiszolgálni a kéréseket, a következő címek használatával:

http://localhost:8888/application-default.properties
http://localhost:8888/application-default.yaml

Természetesen Git repository-t használva képesek vagyunk kiszolgálni több alkalmazást, vagy ugyanazon alkalmazás több példányát is (egy, de akár több repo-t használva is). Az adott példánál maradva legyen az, hogy az alkalmazást telepítjük teszt környezetben is, a hozzá tartozó konfigurációkat tartalmazza a Git repository-ban az application-test.properties állomány. Ekkor látható, hogy a http://localhost:8888/application/test címen már bejön az új property. A klienst indítsuk a test profile-lal, ehhez elegendő ezt parancssorban definiálni a -Dspring.profiles.active=test paraméterezéssel. Az alkalmazást újraindítva látható, hogy már az új paramétert vette fel.

A Config Server képes a jelszavak titkosított tárolására is. Példánkban alkalmazzunk egy szimmetrikus kulcsú titkosítást. Ehhez egyrészt meg kell adni a kulcsot, másrészt telepíteni kell a JCE Unlimited Strength Jurisdiction Policy állományokat - ez utóbbi az amerikai exportkorlátozások feloldására. A kulcs megadása a Config Server application.properties állományában kell megadni encrypt.key értékkel. A sikeres beállítás a http://localhost:8888/encrypt/status címen ellenőrizhető. Amennyiben ez nincs megadva, a következő hibaüzenetet kapjuk:

{"description":"No key was installed for encryption service","status":"NO_KEY"}

Amennyiben a licensz nincs jól telepítve, a következő exception jelenik meg.

java.security.InvalidKeyException: Illegal key size
at javax.crypto.Cipher.checkCryptoPerm(Cipher.java:1039) ~[na:1.8.0_71]

A jelszavak titkosítása ezután elég érdekes módon történik, a Config Server biztosít egy /encrypt címet, erre kell elküldeni POST metódussal a jelszót, és visszaadja titkosítva. A következő parancs használható:

curl localhost:8888/encrypt -d 'Hello Secret!'

A visszaadott érték a Git repository-ban a properties fájlba a {cipher} előtaggal kerüljön.

message = {cipher}24768144acc70fcd738c5a5bbf8c8fc9a9b53ca1de2cf1a4ba0ad4e9b0d5eeb4

A Spring Cloud Config legérdekesebb tulajdonsága azonban a konfigurációk újratöltése. Ez gyakorlatilag azon Spring beanekre vonatkozik, melyen szerepel a @RefreshScope annotáció. Ez amúgy a spring-cloud-context artifactban van. A kliensben megjelent egy http://localhost:8080/refresh szolgáltatás, mely meghívására újratölti az értékeket, ezt kell POST metódussal meghívni.

curl -X POST http://localhost:8080/refresh

Ez a háttérben úgy működik, hogy a Spring húz egy AOP proxy-t a bean köré, melynek azután delegálja a hívásokat. A refresh hatására törli belső bean referenciáját, és a beanre (azaz a proxy-ra) érkező következő hívásnál újra példányosítja azt, és elvégzi az új konfigurációval a dependency injectiont. Ez szépen látszódik, hogy a következő hívásnál a beanen meghívásra kerülnek a @PostConstruct annotációval ellátott metódusok.

Ahhoz, hogy a Config Serveren történt változások is automatikusan eljussanak a kliensekhez, egy sokkal bonyolultabb infrastruktúrát kell biztosítani. Szükséges egy message oriented middleware, mely a publish és subscribe modellt is meg tudja valósítani. Amennyiben konfigurációs változás történt, a Server egy üzenetet küld az összes kliensnek, melyre azok fel vannak iratkozva, és az üzenet hatására újraolvassák a konfigurációt. Ezen modell megvalósítására való a Spring Cloud Bus, egy pehelysúlyú message broker, ami azonban egyelőre AMQP brókerekhez tartalmaz kapcsolódási lehetőséget, jelenleg Rabbit MQ-hoz és Kafkához.

Telepítsünk tehát először egy Rabbit MQ-t. Ez Erlangban implementált, ezért az is szükséges hozzá.

sudo apt-get install erlang erlang-doc

A RabbitMQ-t letöltés után csak ki kell csomagolni, és a sbin/rabbitmq-server parancs hatására villámgyorsan elindul.

A szerveren fel kell venni a org.springframework.cloud:spring-cloud-config-monitor függőséget, mely figyeli a konfiguráció változást, valamint a org.springframework.cloud:spring-cloud-stream-binder-rabbit a Rabbit MQ-hoz való integrációhoz. A szervert újraindítva ilyen érdekes üzenetek jelennek meg a naplóban:

2016-09-04 21:03:12.095  INFO 25005 --- [           main] o.s.a.r.c.CachingConnectionFactory       : Created new connection: SimpleConnection@5fb3111a [delegate=amqp://guest@127.0.0.1:5672/, localPort= 36798]
2016-09-04 21:03:12.264  INFO 25005 --- [           main] o.s.c.c.m.FileMonitorConfiguration       : Monitoring for local config changes: [/home/jtechlog/config]

Semmit nem kell konfigurálni, alapból megtalálja az alapértelmezett konfigurációval indított Rabbit MQ-t.

Egyrészt látszik, hogy csatlakozott a Rabbit MQ-hoz, valamint figyeli a konfiguráció változását a fájlrendszerben. Amennyiben távoli Git repository-ban vannak a konfigurációs fájlok, be kell konfigurálni egy web hookot, mely a http://localhost:8888/monitor címre posztol változás esetén. Erre most nincs szükség.

Kliens oldalon elég a org.springframework.cloud:spring-cloud-starter-bus-amqp függőség felvétele. Újraindítva azt tapasztalhatjuk, hogy amennyiben változtatjuk a konfigurációt, az automatikusan érvényre jut.

Ha egy kicsit meg akarjuk érteni, mi történik, kapcsoljuk be, hogy a kliens trace-elje a Spring Cloud Bustól kapott üzeneteket. Ez az application.properties-ben megadható a spring.cloud.bus.trace.enabled = true megadásával. Valamint kell egy trace implementáció is, a következőt használhatjuk a ClientApplication osztályban:

@Bean
public TraceRepository traceRepository() {
	InMemoryTraceRepository inMemoryTraceRepository = new InMemoryTraceRepository();
	inMemoryTraceRepository.setCapacity(10);
	return inMemoryTraceRepository;

}

Ekkor a http://localhost:8080/trace címen nyomon követhetjük a http hívásokat, de busról származó üzeneteket is.

Végül implementáljunk egy admin alkalmazást, mely képes nyomon követni a Spring Bootos alkalmazásainkat. Ehhez a spring-boot-admin projektet használjuk, mely Spring Bootra épít, AngularJS felülettel. Ehhez megint generáljunk egy projektet, most csak a Web függőséggel, és adjuk hozzá két függőséget: de.codecentric:spring-boot-admin-server:1.4.1 és de.codecentric:spring-boot-admin-server-ui:1.4.1. Az application.properties fájlban írjuk át, hogy a 8081-es porton fusson (server.port = 8888). Az AdminApplication osztályban használjuk az @EnableAdminServer annotációt.

A szerver és kliens alkalmazásokban fel kell venni a de.codecentric:spring-boot-admin-starter-client:1.4.1 függőséget, valamint az application.properties fájlban az admin alkalmazás elérését: spring.boot.admin.url = http://localhost:8081.