Auteurs: Niels de Troije is DevOps engineer en werkzaam voor Rabobank
Robert-Jan Peters is freelance solution architect en werkzaam voor Rabobank
Twee jaar geleden zijn we bij de Rabobank begonnen met het ontwikkelen van microservices met een bijbehorend cloud platform. Vele leuke bestaande en nieuwe problematieken hebben we daarbij opgelost. Dit artikel bespreekt tot in detail een aantal zaken over onze opzet, leerpunten en tips ten aanzien van het scheiden van code en configuratie in een microservice setting met Spring Cloud Config Server.
Het flexibel kunnen managen van properties voor een applicatie is belangrijk. Ten eerste wisselen de waardes van properties veelal per stage van de OTAP. Ten tweede weet een applicatie in een cloud platform zelf niet, hoe de infrastructuur waarop het draait, is ingericht. Het is dan ook niet mogelijk om applicatie property files mee te leveren, die horen bij een release.
Aangezien wij al Spring gebruiken in onze Java programmatuur, was het Spring Boot framework de eerste keus voor de indertijd nieuw te ontwikkelen microservice applicaties. Op de vraag: “op welke manier wordt de distributie van applicatie properties in een gedistribueerde omgeving geregeld?” biedt Spring namelijk een mooie oplossing: Spring Cloud Config Server.
De werking van Spring Cloud Config beschrijven we aan de hand van een eenvoudig voorbeeld (zie Listing 1). De code definieert een REST controller, die een string teruggeeft. De applicatie heeft een variabele declaratie van type String mijnOmgeving, die een waarde moet krijgen van de Spring property in definitie application.environment. Standaard zal Spring deze variabele met de gewenste waarde injecteren bij het starten van de applicatie.
Om uit te leggen hoe dit werkt, zullen we eerst de normale, generieke manier doornemen hoe Spring Boot de waarde van de benodigde properties oplost.
Listing 1
@RestController
public class OmgevingController {
@Value(“${applicatie.environment}”)
private String mijnOmgeving;
@RequestMapping(“/”)
public String index() {
return “Dit is de versie voor ” + mijnOmgeving;
}
}
</code>
Generieke werking springboot properties
Voor het opzoeken van properties hanteert Spring een hiërarchie van 17 stappen om te kijken wat de waarde moet zijn. De hele lijst van 17 stappen vind je hier.
Een aantal belangrijke niveaus zijn:
Lagere nummers hebben hierbij voorrang ten opzichte van hogere nummers.
Via profielen kunnen applicatie properties specifieker worden toegekend aan het gebruik. Bijvoorbeeld de gedetailleerdheid van logging, “DEBUG” bij ontwikkelling en “ERROR” voor productie. Voor het beschrijven van de applicatie properties wordt de yaml structuur gehanteerd.
Onze voorbeeldapplicatie heeft twee property-bestanden: de standaard application.yml en een profiel voor de acceptatie omgeving.
application.yml :
applicatie.environment: test
application-acceptatie.yml:
applicatie.environment: acceptatie
De applicatie start met de volgende commandline:
java -Dspring.profiles.active=acceptatie -Dapplicatie.enviroment=prod -jar demo-app.jar
Op basis van de eerder genoemde hiërarchie zal Spring de -D optie eerder hanteren (nummer 9), dan de applicatie-acceptatie.yaml file (nummer 12 en 13). Deze zal weer eerder dan de application.yaml file (nummer 14 en 15) komen. Het resultaat voor de parameter mijnOmgeving is in dit voorbeeld prod. Hoewel het profile acceptatie is meegegeven, zal de -Dapplicatie.environment optie het winnen.
Een applicatie kan meerdere profiles hebben via een kommagescheiden en gespecificeerd in spring.profiles.active argument. Dit argument is een aanvulling vanuit Spring Boot. Als er meerdere profiles zijn, zal de meest rechtse winnen.
Ok, maar hoe werkt dat dan met Spring Cloud Config? Het Spring Cloud Config systeem is opgebouwd via een client-server concept, waar op basis van een REST interface de Spring Cloud Config Client applicaties hun properties kunnen opvragen aan de Spring Cloud Config Server. Deze haalt de properties vervolgens uit een backing store (git, files, vault). We werken hieronder eerst de serverkant uit en daarna de clientkant.
De specifieke servers waar client applicaties in een cloud platform draaien, zijn niet vooraf te bepalen. Hiermee is de benodigde configuratie via het meeleveren van property files niet haalbaar. Er is dus behoefte aan een centrale configuratieserver, waarin alle benodigde properties zijn opgenomen. Daarbij heeft dit het voordeel, dat bij een wijziging van een property deze direct beschikbaar is voor de applicaties zonder herinstallatie, ongeacht waar de applicatie terecht is gekomen in de cloud.
De vorige paragraaf laat zien dat er verschillende manieren zijn om de configuratie van een applicatie in te stellen door onder andere het gebruik van config (yaml) files. Natuurlijk biedt Spring wel de mogelijkheid om de applicatie te configureren via system properties of environment variabelen (nummer 9, 10). Echter heeft een applicatie meestal een grote hoeveelheid aan properties van verschillende aard (o.a. database, connectie, logging en beveiligingsinstellingen). Een beheerbare en overzichtelijke manier van leveren is vereist en hiervoor biedt Spring Cloud Config Server de helpende hand.
De config server heeft toegang tot een zogenaamde repository en haalt de verschillende property bestanden uit die repository. Een optie voor de repository is git, zodat de properties meteen onder versiebeheer staan. Ook andere mogelijkheden zijn beschikbaar, zoals bestanden op een filesysteem of Hashicorp Vault. Iedere applicatie dient een unieke identificatie (spring.application.name) te krijgen, waaronder de configuraties op te halen te zijn.
Laten we het iets concreter maken: een Spring Cloud Server instantie. Spring Cloud Config Server is zelf ook een Spring Boot applicatie en de code om deze te bouwen staat in Listing 2. Het opnemen van een dependency op spring-cloud-starter en een annotatie @EnableConfigServer is voldoende.
Om bij het opstarten de locatie van de repository te bepalen, is er nog wel een opstart parameter nodig: namelijk de locatie van de repository. In dit voorbeeld gebruiken we git als backing store repository.
java -Dspring.cloud.config.server.git.uri=https://git.xxxx.xx/your-config-repo config-server.jar
Dit is alles en een draaiende configuratie server met een git repository is nu actief.
Listing 2
@SpringBootApplication
@EnableConfigServer
public class ConfigServer {
public static void main(String[] args) {
SpringApplication.run(ConfigServer.class, args);
}
}
De volgende stap is om de configuratie files in git te krijgen, zodat ze door de config server kunnen worden uitgeserveerd aan de client applicaties.
In de paragraaf over spring property configuratie heb je kunnen lezen, dat een applicatie geconfigureerd kan worden met behulp van application.yml files en profile specifieke application-profile.yml files. De profile specifieke files hebben voorrang boven de algemene application.yml. Deze structuur herhalen we in de git repository. We maken voor elke applicatie een eigen application.yaml en eventueel profile specifieke application.yaml files. Deze files kunnen natuurlijk niet application.yaml heten, want dan kun je maar één applicatie in je backing store hebben.
De application.yml file voor een applicatie in de git repository van de config server heeft een unieke identificatie van de Spring application, die wordt gespecificeerd door de parameter spring.application.name. In het volgende voorbeeld met de naam van de applicatie mijnApp, zal de file mijnApp.yml worden geserveerd als application.yml. De profile specifieke files heten in de git repository mijnApp-your-profile.yml
De onderliggende repository bevat bijvoorbeeld de volgende configuratie bestanden:
mijnApp.yml
mijnApp-dev.yml
mijnApp-test.yml
mijnApp-acceptatie.yml
mijnApp-prod.yml
Hierbij bevat de mijnApp.yml de algemene waardes van de properties, die voor alle profielen gelden zijn en de andere profielen de juiste waardes bevatten, die context- of omgevings-specifiek zijn.
In een standaard OTAP-infrastructuur levert dit al 5 bestanden per applicatie op. Met een microservices benadering zal de groei van het aantal applicatie instanties toenemen en daarmee ook het aantal configuratiebestanden. In een platte bestandsstructuur wordt dit onoverzichtelijk en ontstaat de behoefte aan een meer gelaagde structuur.
De introductie van een subdirectory met de naam van de applicatie biedt de mogelijkheid om de configuratiebestanden te organiseren. In het genoemde voorbeeld wordt dit dan:
mijnApp
mijnApp.yml
mijnApp-prod.yml
Echter is dit niet de standaard werking en de server moet dit zoekpad nog weten. Via een extra opstart configuratie parameter spring.cloud.config.server.git.searchPaths={application} kan het zoekpad worden ingesteld en uitgebreid. Drie parameters zijn nu gespecificeerd om de juiste application.yaml files te vinden: we weten de locatie van de git repository, we weten de applicatienaam en we hebben gezegd dat de files per applicatie te vinden zijn in een subdirectory, waarvan de naam gelijk is aan de applicatienaam.
Op deze manier worden configuratiebestanden van een applicatie overzichtelijk gehouden (directory structuur), zijn alle wijzigingen traceerbaar (git), beheersbaar en direct toepasbaar in een gedistribueerde omgeving (via config server).
Een leuk onderdeel van de config server is dat het ook platte tekstfiles kan serveren. Je kunt bijvoorbeeld ook logback bestanden laten serveren en dus je logback configuratie via de config server laten lopen. Breng hiervoor de file (in ons geval de logback.xml file en de bijbehorende profiel-specifieke logback bestanden) onder in de juiste directory voor de client applicatie.
mijnApp:
logback.xml
logback-log-ota.xml
logback-log-prod.xml
Ook voor platte tekst geldt, dat de server een algemeen bestand, maar ook profiel-specifieke bestanden kan serveren. De configuratie van de client applicatie heeft dan een extra configuratieparameter nodig:
logging:
config: ${spring.cloud.config.uri}/${spring.application.name}/${spring.profiles.active}/master/logback.xml
Hoewel hier alleen de logback.xml wordt genoemd, zal de server toch bijvoorbeeld een logback-log-ota leveren als de Spring profile lijst van de applicatie log-ota bevat.
Vaak wordt een applicatie gestart op basis van een combinatie van profielen. Naast de gangbare OTAP-instellingen worden ook vaak tool-specifieke settings aangemaakt, zoals voor onder andere ELK-stack (Elastick Logstash Kibana), Spring Cloud Sleuth of Docker. Niet altijd is duidelijk wat de uiteindelijke configuratie is, die de server voor een applicatie en bijbehorende profielen heeft opgebouwd.
In het eindresultaat kijken of een URL naar een backend wel correct staat, is dan noodzakelijk bij een probleemdiagnose. Hoewel alle bestanden van een applicatie in git staan en deze handmatig samengesteld kunnen worden, is het fijner om te kijken wat de server teruggeeft aan opgetelde configuratie voor de applicatie en de specifieke spring profiles.
Binnen de Rabobank hebben we daarvoor binnen de server een config browser functionaliteit gebouwd. De code daarvan voert een beetje te ver om in dit artikel te gaan beschrijven, maar gelukkig is er ook een eenvoudige manier om via een REST interface de configuratie uit te vragen.
Dat levert een JSON-output op met het door de config server opgestelde resultaat van alle profielen (drie in dit voorbeeld) opgeteld bij de algemene application.yml van deze applicatie.
We hebben nu een werkende config server, met een backing store repository (git). De volgende stap is een client applicatie hieraan te koppelen. Voor een applicatie, die gebruik wil maken van de services van de config server, zijn drie eenvoudige stappen nodig:
Het opnemen van de benodige dependecy ziet er als volgt uit:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
Start de applicatie met extra -D opties:
-Dspring.application.name=myApp
-Dspring.cloud.config.uri=http://config-server-ota.xxxx.xx/
-Dspring.profiles.active=ota
Met het starten van de applicatie wordt de Spring Cloud Client geactiveerd en deze maakt een verbinding met de config server om de samengestelde configuratie op te halen. Zie Listing 3 van startup logregels.
Listing 3
2018-07-04 08:12:40.082 INFO 7458 — [ main] c.c.c.ConfigServicePropertySourceLocator : Fetching config from server at: http://config-server-ota.xxx.xxxx/
2018-07-04 08:12:40.996 INFO 7458 — [ main] c.c.c.ConfigServicePropertySourceLocator : Located environment: name=myApp, profiles=[ota], label=null, version=3a1c9122d55b33e4e7d3e8cb4f81f68f9e256f23, state=null
2018-07-04 08:12:40.997 INFO 7458 — [ main] b.c.PropertySourceBootstrapConfiguration : Located property source: CompositePropertySource [name=’configService’, propertySources=[MapPropertySource {name=’configClient’}, MapPropertySource {name=’ssh://git@git.xxx.xxx:ppp/repo/configuration.git/file:/home/vcap/app/basedir/myApp/myAppp-ota.yml’}, MapPropertySource {name=’ssh://git@git.xxxx.xxx:pppp/repo/configuration.git/file:/home/vcap/app/basedir/myApp/myApp.yml’}]]
2018-07-04 08:12:41.004 INFO 7458 — [ main] hello.Application : The following profiles are active: ota
We hebben nu een werkende config server en we hebben er een client applicatie aan gekoppeld. De properties worden nu uit git gehaald en via de config server geleverd aan de client. Maar we hebben nog niet naar de veiligheid gekeken. In onze voorbeelden wordt bijvoorbeeld HTTP gebruikt in plaats van HTTPS. Eventuele wachtwoorden worden opgeslagen in git en via onbeveiligde protocollen geleverd aan de client applicatie. We hebben dus enkele beveiligingsissues op te lossen.
Zoals eerder genoemd start de Spring Cloud Config Server op met een parameter van de repository.
-Dspring.cloud.config.server.git.uri=https://git.xxx.xx
Dit voorbeeld gebruikt wel HTTPS, maar zonder authenticatie. De onderliggende git repository moet dus nog steeds op ”public” staan en de gevoelige gegevens zijn voor iedereen te benaderen in git zelf. Daarnaast biedt de server ook een REST interface, waarmee de configuratie onbeveiligd is uit te lezen. Op welke manier is het mogelijk om dit veilig en beheersbaar op te lossen?
Voor het beveiligen van de integratie tussen de config server en de git repository zijn er drie mogelijkheden.
Een eerste mogelijkheid is om de URI te specificeren als https://user:password@git.xxxx.xx. Een nadeel van deze methode is dat de username en het password zichtbaar zijn. Deze zijn alsnog te eenvoudig te achterhalen in een runtime omgeving of op de commandline op Linux, mits je daar bij kunt komen natuurlijk.
De tweede, veiligere mogelijkheid is om de username en password te specificeren in de application.yml, die is gepackaged in config server.
spring:
cloud:
config:
server:
git:
uri: https://git.xxx.xx/git-project/configuration-repo.git
username: gebruiker
password: wachtwoord
Het achterhalen van username/wachtwoord is dan iets moeilijker geworden dan in het eerste voorbeeld.
Een derde mogelijkheid is om SSH-verbinding naar git te gebruiken in plaats van HTTPS.
Voor het gebruik moet eerst een SSH keypair aanwezig zijn, bestaande uit een public/private key. Het genereren van zo’n keypair valt buiten de scope van dit artikel. Meer informatie daarover kun je vinden op www.ssh.com/ssh/keygen.
De betreffende git repository van de applicatie wordt geconfigureerd met de public key. De actie is afhankelijk van het onderliggende product, zoals Github of Bitbucket. De private key specificeer je in de application.yml van de server zelf en dit gaat als volgt:
spring.cloud.config.server.git:
uri: ssh://git@git.xxxx.xx:ssh-port/git-project/configuration-repo.git
ignoreLocalSshSettings: true
strictHostKeyChecking: false
privateKey: |
—–BEGIN RSA PRIVATE KEY—–Hier je private key
—–END RSA PRIVATE KEY—–
De URI is nu omgezet naar een SSH-verbinding. De private key is gespecificeerd en twee extra instellingen zijn gedaan om git te kunnen gebruiken met de parameters in de application.yml in plaats van via je onderliggende operating system. Ook aan deze methode kleven nog nadelen, maar de drempel hiervan is hoger door een splitsing van kennis en bezit.
Nu kan de inhoud van de git repository veilig worden aangeboden en kan deze van “public” af.
De server zelf is standaard anoniem te benaderen. De REST interface is nog met bijvoorbeeld postman of curl uit te vragen en eventuele wachtwoorden in de configuratie zijn nog steeds niet voldoende beveiligd.
De server is ook een Spring Boot applicatie en via de standaard Spring functionaliteit is deze door Spring Security eenvoudig te beveiligen. Hiervoor is het alleen nodig om een dependency op te nemen en de beveiliging is dan geactiveerd. Omwille van het voorbeeld wordt alleen stilgestaan bij Basic Authentication, maar Spring Security biedt een veel andere mogelijkheden. Specificeer vervolgens de onderstaande instellingen in de config server application.yml:
spring.security.user.name=gebruiker
spring.security.user.password=wachtwoord
En in de bootstrap.yml (!) van de client applicatie:
spring.cloud.config:
uri: https://config-server.xxxx.xx
username: gebruiker
password: wachtwoord
De git repository staat nu dicht, de server maakt contact via beveiligde SSH-verbinding, de server zelf is beschermd en de client kan via Basic Authentication alsnog een verbinding maken. Hierdoor is de REST interface ook beveiligd. Belangrijk om te vermelden: hoewel we hier een username/password gebruiken, is dit toch enigszins veilig, omdat ze opgeslagen zijn in de runnable jar zelf en niet in de git repo’s.
Voor de volledigheid willen we hier ook nog even noemen dat Spring Cloud Config Server de mogelijkheid heeft om properties (zoals passwords) encrypted op te slaan in de git repo en ze on-the-fly unencrypted uit te serveren naar de client applicatie. Verder is het zelfs mogelijk om de properties encrypted volledig door te sturen naar de client applicatie en ze daar te laten decrypten. Dat is de laatste stap in het veiliger maken van het uitleveren van properties in een cloud omgeving.
In dit artikel hebben we stil gestaan bij een opzet om properties voor applicaties gebaseerd op Springboot op een beheersbare en veilige manier aan te bieden in een Cloud platform. Het is niet haalbaar om in detail op alle punten in te gaan, maar hopelijk hebben we voldoende inspiratie geboden om hiermee aan de slag te gaan.