Je kunt er tegenwoordig niet omheen: cloud architectuur en microservices. Voor sommigen voelen ze als modewoorden: de termen worden te pas en te onpas gebruikt. Vaak zonder context of met uiteenlopende definities. Voor veel ontwikkelaars is het onduidelijk wat de impact op de applicatie en de architectuur is. Of op de eigen kennis en carrière op de lange termijn. Het is lastig om te bepalen op welke manier ze hun vaardigheden kunnen verbeteren op dit vlak.
Cloud architectuur en microservices zijn manieren om complexiteit te beheersen en gelijktijdig problemen op het gebied van schaalbaarheid en beschikbaarheid op te lossen. Het zijn absoluut geen ‘silver bullets’ en ze zullen altijd de nodige complexiteit houden. In een dergelijke moderne architectuur met veel verschillende microservices loop je al snel tegen vaak voorkomende problemen aan. Zoals: waar laat je je configuratie, hoe detecteer je andere services, wat doe je als een service uitvalt en hoe verdeel je de load?
Voor cloud native microservices of applicaties, in essentie gedistribueerde systemen, heb je een aantal basisvoorzieningen nodig, zoals een service registry en load balancing. Het is ook goed je configuratie buiten de applicatie te bewaren en deze beschikbaar te stellen aan alle services. Deze patronen helpen om de complexiteit zoveel mogelijk te beheersen.
Als ontwikkelaar kun je met Spring Cloud eenvoudig applicaties ontwikkelen die op deze patronen en oplossingen gebaseerd zijn. Die applicaties kunnen zowel op je lokale ontwikkelomgeving als op een traditionele infrastructuur en cloud platformen correct werken. In de komende paragrafen kijken we hoe we een eerste stap maken in het opzetten van deze Spring Cloud-applicaties.
Configuratie centraal beheren
Er zijn een aantal manieren om een applicatie te voorzien van configuratie. In traditionele applicaties wordt de configuratie vaak meegeleverd als bestand in de applicatie (bijvoorbeeld een .properties) of er wordt verwezen naar een bestand buiten de applicatie.
Op een cloud infrastructuur waar meerdere instanties dynamisch worden gecreëerd en verwijderd, is dit moeilijk te beheren. In dit geval is een centrale plek waar de applicaties hun configuratie op kunnen vragen vaak handiger. In een continuous delivery omgeving heeft dit ook voordelen. Bestanden zijn eenvoudig te beheren in versiebeheer in plaats van statisch samengesteld bij deployment.
Configuration server opzetten
Wanneer je spreekt over een configuration server heb je het over een aparte microservice die ervoor zorgt dat andere microservices hun configuratie kunnen opvragen. In plaats van het meeleveren van configuratie in de verschillende microservices, plaats je de configuratie op een centrale locatie. Deze locatie kan een Git repository, bestandssysteem of het classpath van je configurationserver zijn. Hiermee houd je de code en configuratie gescheiden en is de configuratie centraal te beheren. Wijzigingen kunnen ook direct worden uitgerold.
De configuration server laadt de configuratie in en stelt deze beschikbaar aan andere microservices. Idealiter wordt hierbij gebruik gemaakt van een Git repository hierbij als locatie (zie Figuur 1). Het grootste voordeel hiervan is dat wijzigingen direct effect hebben op de services zonder te hoeven herstarten of opnieuw deployen. Bovendien heb je op je configuratiebestanden direct autorisatie en versiebeheer.
De configuration server is een Spring Boot-applicatie die je eenvoudig kunt configureren met slechts één annotatie: @EnableConfigurationServer, zoals in Listing 1.
@SpringBootApplication
@EnableConfigurationServer
public class ConfigServerApplication {
public static void main(String[] args) {
SpringApplication.run(ConfigServerApplication.class, args);
}
Listing 1: Configuration server.
Daarnaast moet je voor het opstarten van de configuration server een application.yml bestand maken. In dit bestand definieer je wat de locatie van de configuratiebestanden is. In dit voorbeeld (zie Listing 2) laden we voor het gemak de bestanden van het classpath van de configuration server, namelijk de map /config. In plaats hiervan kan je dus ook een Git repository gebruiken. We laten de configuration server standaard op poort 8888 starten om poortconflicten met andere microservices te voorkomen op de lokale ontwikkelmachine.
server.port: ${PORT:8888}
spring:
application.name: sample-config
profiles.active: native
cloud.config.server.native.searchLocations: classpath:/config
Listing 2: application.yml van de configuration server.
Gebruikmaken van de configuration server
Andere microservices kunnen vanaf nu gebruik maken van de beschikbaar gestelde configuratie. Een service haalt de configuratie op van de configuration server via REST. De servicenaam, Spring profiles en een eventueel label worden gebruikt om de juiste configuratie op te halen. Om de configuratie te laden voordat de Spring-applicatie wordt gestart, dien je een bootstrap.yml bestand op het classpath te hebben dat wordt gebruikt om de configuratie te injecteren in de beans. Hierin configureer je de locatie van de configuration server, zie Listing 3.
server.port: ${PORT:8080}
spring:
application.name: demo-service
cloud:
config:
enabled: true
uri: http://localhost:8888
Listing 3: bootstrap.yml om configuratie op te halen voor de microservice.
Nu kun je voor de service de configuratie in de configuration server plaatsen. Een bestand demo-service.yml in de /config map van de configuration server zal door de demo-service applicatie worden opgehaald en ingelezen. De properties die hierin gedefinieerd staan, kunnen worden geïnjecteerd in beans, zoals je gewend bent met lokaal gedefinieerde properties.
Service registration en discovery
Stel je voor: je hebt één of meerdere microservices ontwikkeld die met behulp van bijvoorbeeld REST andere services in het landschap aanroepen. In een traditionele infrastructuur configureer je de benodigde URLs eenmalig in de configuratie van de services zélf. Wanneer je overstapt op een cloud architectuur met meerdere instanties van dezelfde service die automatisch op- en afgeschaald kunnen worden, is statische configuratie van de te gebruiken services vaak niet meer wenselijk.
Hier komt het concept van service discovery om de hoek kijken. Er zijn twee verschillende methoden: server side discovery en client side discovery.
Je kan gebruik maken van server side discovery waarbij service-instanties zichzelf registreren bij een centrale registry en je deze configureert als entry point naar een service (een traditionele load balancer). In dit geval weet de aanroepende service niet welke service het verzoek afhandelt.
Een andere strategie is client side discovery waarbij de aanroepende service weet welke service-instanties er zijn. De service haalt informatie over de beschikbare instanties uit de service registry, voordat een verzoek naar een service wordt gestuurd (zie Figuur 2).
Deze strategie is ten opzichte van server side discovery gemakkelijker te realiseren. Het legt de verantwoordelijkheid van het service registry lookup en load balancen bij de client. Dat brengt een aantal voordelen met zich:
- Er is géén centrale router of load balancer nodig.
- De services blijven functioneren (door lokaal te cachen) zelfs wanneer de service registry even niet beschikbaar is.
In dit voorbeeld gaan we een client side discovery implementeren met Spring Cloud. Er zijn verschillende implementaties beschikbaar voor service discovery, waaronder Eureka al dan niet met Ribbon als client side load balancer. Deze tools zijn door Netflix ontwikkeld en worden door Spring Cloud als implementaties ondersteund.
Service registry opzetten met Eureka
Eureka wordt uitgerold als zelfstandige service in de cloud. In een productieomgeving zal je waarschijnlijk meerdere Eureka nodes willen draaien voor high availability. Er zijn overigens verschillende PaaS providers die Eureka als standaard service beschikbaar stellen, waardoor je deze niet zelf hoeft te bouwen en te deployen. Een Eureka server configureren kan heel eenvoudig in een Spring Boot-applicatie, zoals getoond in Listing 4.
@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class, args);
}
}
Listing 4: Eureka server
Registreren van service in Eureka
Elke service dient zich te registreren bij de Eureka server. Door de annotatie @EnableEurekaClient toe te voegen op je Spring Boot-applicatie, registreert deze zich automatisch als service (in dit voorbeeld demo-service) bij Eureka. De geregistreerde microservice haalt ook meteen een lijst op met geregistreerde services. Een client side load balancer kan vervolgens gebruik maken van deze lijst met geregistreerde services.
Load balancing met Ribbon
Om een andere service aan te roepen, kan je Ribbon gebruiken. Deze werkt goed samen met Eureka. Dit is een client side load balancer die de lijst van actieve service-instanties uit Eureka ophaalt en daar verzoeken over verdeelt volgens een te configureren strategie.
In de praktijk kan je dit testen door een REST-controller toe te voegen die bijvoorbeeld de tijd laat zien (zie Listing 5).
@RestController
public class TimeController {
@Value("${eureka.instance.instanceId}")
private String instanceId;
@RequestMapping("/time")
public String getTime() {
return "Instance " + instanceId + ": it is now "
+ Instant.now().toString();
}
}
Listing 5: Voorbeeld controller
Om te kunnen zien welke instantie het verzoek heeft afgehandeld, nemen we de waarde van property eureka.instance.instanceId op in het antwoord. Deze is automatisch beschikbaar als je de Eureka client gebruikt. Dan schakel je de Ribbon client in en kan je de service aanroepen door gebruik te maken van RestTemplate (zie Listing 6).
@SpringBootApplication
@EnableEurekaClient
@EnableScheduling
public class DemoClientApplication {
@Autowired
private RestTemplate restTemplate;
@Scheduled(fixedRate = 1000)
public void run() {
restTemplate.getForObject(
“http://demo-service/time”,
String.class);
}
}
Listing 6: Ribbon client
In plaats van de fysieke host name kun je de naam van de service gebruiken, zoals deze is geregistreerd in Eureka. Ribbon zal de servicenaam vertalen naar één van de actieve instanties. Als je meerdere instanties van de service opstart dan zal je zien dat de verzoeken telkens door een andere instantie worden afgehandeld.
Standaard zal Eureka via de client periodiek een heartbeatcontrole uitvoeren om te bepalen of de instantie nog actief is. Elke Spring Boot-applicatie biedt automatisch een /health endpoint aan waarmee de Eureka server periodiek controleert of de instantie nog 'gezond' genoeg is om verzoeken te kunnen afhandelen. Als dat niet het geval is, zal Eureka deze instantie uit de lijst met actieve clients halen.
Conclusie
Je hebt gezien dat cloud architectuur niet zomaar een modewoord is, geen magie hoeft te zijn en dat je met Spring Cloud een applicatie kan maken die zich goed gedraagt in een dergelijke omgeving. Het loskoppelen van code en configuratie en het opzetten van een service registry om verschillende microservices met hun eigen verantwoordelijkheid elkaar te laten vinden is met Spring Cloud eenvoudig. Slechts met een aantal extra dependencies en annotaties kunnen we veel voorkomende cloud architectuur patronen implementeren. Een valkuil is om snel met deze details aan de slag te gaan en het complete architectuuroverzicht uit het oog te verliezen. Houd dit vooral in het achterhoofd tijdens het implementeren.
Spring Cloud biedt ondersteuning voor nog meer patronen, zoals circuit breaker, authenticatie, messaging, routing en ook integraties met veel cloud providers zoals Amazon, CloudFoundry en Heroku. Op http://projects.spring.io/spring-cloud/ kan je hier heel veel over vinden.
Alle code en een werkend voorbeeld is te vinden op BitBucket: https://bitbucket.org/jdriven/spring-cloud-example.