13:00 — 13:15 Developing with Spring Cloud [Lecture]
13:15 — 13:45 Service Registration and Discovery [Lecture]
13:45 — 14:15 Simple Discoverable applications [Lab]
14:15 — 15:15 Service Discovery in the Cloud [Lab]
15:15 — 15:45 Zero-Downtime Deployments for Discoverable services [Lab]
15:45 — 16:00 Break
16:00 — 16:30 Configuration Management [Lecture]
16:30 — 17:00 Configuration Management in the Cloud [Lab]
17:00 — 17:15 Zuul [Lecture / Lab]
## Developing with Spring Cloud
- Introduction to Spring Cloud and why it exists
- Spring Cloud OSS and Spring Cloud Services (PCF Tile)
- SCS gives the ability to have our applications talk each directly without going thru the router. To do that we need have an application setting called
spring.cloud.services.registrationMethod. The values for this setting are :routeanddirect. If we useroute(the default value), our applications register using their PCF route else if they register using their IP address.
NOTE: To enable direct registration, you must configure the PCF environment to allow traffic across containers or cells. In PCF 1.6, visit the Pivotal Cloud Foundry® Operations Manager®, click the Pivotal Elastic Runtime tile, and in the Security Config tab, ensure that the “Enable cross-container traffic” option is enabled. cf-demo-client ----{http://demo/hi?name=Bob }--> cf-demo-app <----{`hello Bob` }------------- First we are going to get our 2 applications running locally. With our local Eureka server. And the second part of the lab is to push our 2 applications to PCF and use PCF Service Registry to register our applications rather than our standalone Eureka server.
- You will need JDK 8, Maven and STS. If you don't use STS, you need to go to Spring Initilizr to create your projects.
- git clone https://github.com/MarcialRosales/spring-cloud-workshop
Go to the folder, labs/lab1 in the cloned git repo.
Run eureka-server (from STS boot dashboard or from command line)
Go to the eureka-server url:
http://localhost:8761/Run cf-demo-app
Check that our application registered with Eureka via the Eureka Dashboard
Check that our app works
curl localhost:8080/hello?name=MarcialRun cf-demo-client
Check that our application works, i.e. it automatically discover our demo app by its name and not by its url.
curl localhost:8081/hi?name=BobCheck that our application can discover services using the
DiscoveryClientapi. `curl localhost:8081/service-instances/demo | jq .``stop the cf-demo-app
Check that it disappears from eureka but it is still visible to the client app.
curl localhost:8081/service-instances/demo | jq .After 30 seconds it will disappear. This is because the client queries eureka every 30 seconds for a delta on what has happened since the last query.stop eureka server, check in the logs of the demo app exceptions. Start the eureka server, and see that the service is restored, run to check it out:
curl localhost:8081/service-instances/demo | jq .
We know our application works, we can push it to the cloud.
Note: Each attendee has its own account set up on this PCF foundation: https://apps.run-02.haas-40.pez.pivotal.io
login
cf login -a https://api.run-02.haas-40.pez.pivotal.io --skip-ssl-validationcreate service (http://docs.pivotal.io/spring-cloud-services/service-registry/creating-an-instance.html)
cf marketplace -s p-service-registrycf create-service p-service-registry standard registry-service
update manifest.yml (host, and CF_TARGET)
push the application
cf pushCheck the app is working
curl cf-demo-app.cfapps-02.haas-40.pez.pivotal.io/hello?name=MarcialGo to the Admin page of the registry-service and check that our service is there
Now we install our client application
update manifest.yml (host, and CF_TARGET)
push the application
Check the app is working
cf-demo-client.cfapps-02.haas-40.pez.pivotal.io/hi?name=MarcialCheck that our app is not actually registered with Eureka however it has discovered our
demoapp.We can rely on RestTemplate to automatically resolve a service-name to a url. But we can also use the Discovery API to get their urls.
curl cf-demo-client.cfapps-02.haas-40.pez.pivotal.io/service-instances/demo | jq .Comment out the annotation @LoadBalanced which decorates a RestTemplate with Ribbon capabilities so that we can use a service-name instead of a URL and push the app. you will see that the first request below but not the second one.
cf-demo-client.cfapps-02.haas-40.pez.pivotal.io/hi?name=Marcialcurl cf-demo-client.cfapps-02.haas-40.pez.pivotal.io/service-instances/demo | jq .
- New Features on Jersey 2.0. Spring Web/REST vs Jersey 2.
- WIP eureka2 project based on Jersey 2.0 (https://github.com/Netflix/eureka/tree/master/eureka-client-jersey2)
- We still have to remove Ribbon transitive dependency on Jersey 1.19. It should be possible to remove it given that it has pluggable transport but it is a big job though.
- If we really want to leverage Netflix's load balancing capabilities the preferred path would be to keep working with Jersey 1 until Netflix updates all its stack to Jersey 2.
We cannot register two PCF applications with the same spring.application.name against the same SCS central-registry service instance (but with different service's bindings or credentials) because according to SCS (1.1 and earlier) that is considered a security breached (i.e. another unexpected application is trying to register with the same name as another already registered application but using different credentials).
To go around this issue, we cannot bind PCF applications (blue and green) to the service instance of the service-registry (p-service-registry) because that will automatically create a new set of credentials for each application.
Instead, we need to ask the service instance -i.e. the service-registry from SCS- to provide us a credential and we create a User Provided Service with that credential. Once we have the UPS we can then bind that single UPS with our 2 applications, green and blue. That works because both instances, even though they are uniquely named in PCF they have the same spring.application.name used to register the app with Eureka and both apps are using the same credentials to talk to the service-registry, i.e. Eureka.
Go to the folder, labs/lab3 in the cloned git repo.
- Create a new manifest and modify the attribute 'name' and change it to
cf-demo-app-greenand push the app. - It will fail because Eureka does not allow two PCF apps to register with Eureka using the same
spring.application.name.
- Create a service instance of the service registry (skip this process if you already have a service instance)
cf create-service p-service-registry standard central-registry - Create a service key and call it
service-registrycf create-service-key central-registry service-registry - Read the actual key contained within the
service-registryservice keycf service-key central-registry service-registry
It prints out something like this:
Getting key service-registry for service instance central-registry as [email protected]...{"access_token_uri": "https://p-spring-cloud-services.uaa.run.haas-35.pez.pivotal.io/oauth/token", "client_id": "p-service-registry-ce80e383-0691-4a0e-a48e-84df7035cb2e", "client_secret": "WGE829u3U7qt", "uri": "https://eureka-c890fdd0-18b5-4c5b-bc44-89ef2383dc08.cfapps.haas-35.pez.pivotal.io" } We have to create a
User Provided Servicewith the credentials above. We will do that briefly.We need to create a custom
EurekaServiceInfoCreatorclass that is able to recognize our newUser Provided Serviceas an Eureka Service. For reference, aServiceInfoCreatoris a Java class of thespring cloud service connectorslibrary which is able to create aServiceInfofrom aVCAP_SERVICESvariable. There are many types of services, for instances, databases, messaging middleware, you name it. For each type of service, there is aServiceInfoCreatorclass. Theconnectorslibrary has a list of thoseServiceInfoCreatorclasses. During the bootstrap process, theconnectorslibrary iterates over the list of services declared in theVCAP_SERVICESvariable. For each service, theconnectorslibrary asks eachServiceInfoCreatorif they recognize that service as of its type. For instance, theEurekaServiceInfoCreatorwill look up the valueeurekain thetagsattribute of the service. If there is a match, theconnectorslibrary asks theEurekaServiceInfoCreatorto create anEurekaServiceInfoinstance which later on it is used to configure theEureka client.
We create a separate java project (cf-demo-connectors) for our custom EurekaServiceInfoCreator class so that we can bundle it with the cf-demo-app and cf-demo-client projects. Both applications will need to bind to the Eureka service therefore they need to find the eureka service in the VCAP_SERVICES.
We need to create a new (text) file that the
connectorslibrary use to identifyServiceInfoCreatorclasses in the class-path. This file must be located undersrc/main/resources/META-INF/services/org.springframework.cloud.cloudfoundry.CloudFoundryServiceInfoCreator. We add the following line to the file:io.pivotal.demo.EurekaServiceInfoCreator. We put this file in the project we created for theEurekaServiceInfoCreator.Now we create a
User Provided Servicewith the credentials above (Remember that we need to add ourlabelattribute)cf cups service-registry -p '{"access_token_uri": "https://p-spring-cloud-services.uaa.run.haas-35.pez.pivotal.io/oauth/token","client_id": "p-service-registry-ce80e383-0691-4a0e-a48e-84df7035cb2e","client_secret": "WGE829u3U7qt","uri": "https://eureka-c890fdd0-18b5-4c5b-bc44-89ef2383dc08.cfapps.haas-35.pez.pivotal.io", "label": "eureka"}'Push your blue app :
cf push -f manifest.yml
... applications: - name: cf-demo-app services: - service-registry ... - Repeat the process with green app:
cf push -f manifest-green.yml
... applications: - name: cf-demo-app-green services: - service-registry ... Both apps have spring.application.name equals to demo.
- Check Eureka dashboard has one entry for our
demoservice with 2 urls, one for blue and another for green.
- We can store our credentials encrypted in the repo and Spring Config Server will decrypt them before delivering them to the client.
- Spring Config Service (PCF Tile) does not support server-side decryption. Instead, we have to configure our client to do it. For that we need to make sure that the java buildpack is configured with
Java Cryptography Extension (JCE) Unlimited Strength policy files. For further details check out the docs.
Go to the folder, labs/lab2 in the cloned git repo.
Check the config server in the market place
cf marketplace -s p-config-serverCreate a service instance
cf create-service -c '{"git":{"uri": "https://github.com/MarcialRosales/spring-cloud-workshop-config" }, "count": 1 }' p-config-server standard config-serverModify our application so that it has a
bootstrap.ymlrather thanapplication.yml. We don't really need anapplication.yml. If we have one, Spring Config client will take that as the default properties of the application.Our repo already has our
demo.yml. If we did not have ourspring.application.name, thespring-auto-configurationjar injected by the java buildpack will automatically create aspring.application.nameenvironment variable based on the env variableVCAP_APPLICATION{... "application_name": "cf-demo-app" ... }.Push our
cf-demo-app.Check that our application is now bound to the config server
cf env cf-demo-appCheck that it loaded the application's configuration from the config server.
curl cf-demo-app.cfapps-02.haas-40.pez.pivotal.io/env | jq .
We should have these configuration at the top :
"configService:https://github.com/MarcialRosales/spring-cloud-workshop-config/demo.yml":{"mymessage": "Good afternoon" }, "configService:https://github.com/MarcialRosales/spring-cloud-workshop-config/application.yml":{"info.id": "${spring.application.name}" }, Check that our application is actually loading the message from the central config and not the default message
Hello.curl cf-demo-app.cfapps-02.haas-40.pez.pivotal.io/hello?name=MarcialWe can modify the demo.yml in github, and ask our application to reload the settings.
curl -X POST cf-demo-app.cfapps-02.haas-40.pez.pivotal.io/refresh
Check the message again. curl cf-demo-app.cfapps-02.haas-40.pez.pivotal.io/hello?name=Marcial
Add a new configuration for production :
demo-production.ymlto the repo.Configure our application to use production profile by manually setting an environment variable in CF:
cf set-env cf-demo-app SPRING_PROFILES_ACTIVE production
we have to restage our application because we have modified the environment.
- Check our application returns us a different value this type
curl cf-demo-app.cfapps-02.haas-40.pez.pivotal.io/env | jq .
We should have these configuration at the top :
Note about Reloading configuration: This works provided you only have one instance. Ideally, we want to configure our config server to receive a callback from Github (webhooks onto the actuator endpoint /monitor) when a change occurs. The config server (if bundled with the jar spring-cloud-config-monitor). If we have more than one application instances you can still reload the configuration on all instances if you add the dependency spring-cloud-starter-bus-amqp to all the applications. It exposes a new endpoint called /bus/refresh . We would only need to go to reach one of the application instances and that instance will propagate the refresh request to all the other instances.
One configuration most people want to dynamically change is the logging level. Exercise is to modify the code to add a logger and add the logging level the demo.yml or demo-production.yml :
logging: level: io.pivotal.demo.CfDemoAppApplication: debug --- spring.profiles: native spring: cloud: config: server: native: searchLocations: ../../spring-cloud-workshop-config Use local git repo (all files must be committed!). One repo for all our applications and each application and profile has its own folder.
--- spring.profiles: git-local-common-repo spring: cloud: config: server: git: uri: file:../../spring-cloud-workshop-config searchPaths: groupA-{application}-{profile} Spring Config server will try to resolve a pattern against ${application}/{profile}
--- spring.profiles: git-local-multi-repos-per-profile spring: cloud: config: server: git: uri: file:../../emptyRepo repos: dev-repos: pattern: "*/dev" uri: file:../../dev-repo prod-repos: pattern: "*/prod" uri: file:../../prod-repo In this case, we have decided to have one repo specific for dev profile and another for prod profilecurl localhost:8888/quote-service2/dev | jq .
--- spring.profiles: git-local-multi-repos-per-teams spring: cloud: config: server: git: uri: file:../../emptyRepo repos: trading: pattern: trading-* uri: file:../../trading pricing: pattern: pricing-* uri: file:../../pricing orders: pattern: orders-* uri: file:../../orders We have 3 teams, trading, pricing, and orders. One repo per team responsible of a business capability.curl localhost:8888/trading-execution-service/default | jq .curl localhost:8888/pricing-quote-service/default | jq .
--- spring.profiles: git-local-one-repo-per-app spring: cloud: config: server: git: uri: file:../../{application}-repo - @EnableZuulProxy
- It automatically (no configuration required) proxies all your services registered with Eureka thru a single entry point. e.g. When the zuul proxy receives this request http://localhost:8082/demo/hello?name=Marcial it automatically forwards this request to http://localhost:8080/hello?name=Marcial
- We can configure Zuul to only allow certain services regardless of the services registered in Eureka. This is done thru simple configuration.
- However, we can customize Zuul internal behaviour. Zuul borrows the Servlet Filters concept from the Servlet specification. Every request is passed thru a number of filters and eventually the request is forwarded to destination, or not. The filters allow us to intercept requests at different stages: before the request is routed, after we receive a response from the destination service. There are special type of filters which we can use to override the routing logic.
The source code for this lab is available under labs\lab4. This lab relies on the previous lab3 artifacts, i.e. cf-demo-app, eureka-server (if you run it locally else Eureka from SCS) and config-server (if you run it locally else Config server from SCS). It also relies on the configuration file demo-gateway.yml in the configuration repository.
- To create a Zuul server we simply create one like this:
@EnableZuulProxy @SpringBootApplication public class GatewayServiceApplication{Map<String, Object> basicCache = new ConcurrentHashMap<>(); @Bean public ZuulFilter histogramAccess(RouteLocator routeLocator, MetricRegistry metricRegistry){return new StatsCollector(routeLocator, metricRegistry)} public static void main(String[] args){SpringApplication.run(GatewayServiceApplication.class, args)} } - We implement our own filter which keeps track of number of requests per service:
class StatsCollector extends ZuulFilter{private static Logger log = LoggerFactory.getLogger(StatsCollector.class); private MetricRegistry metrics; private Map<String,String> serviceAliases = new HashMap<>(); public StatsCollector(RouteLocator routeLocator, MetricRegistry registry){super(); this.metrics = registry; routeLocator.getRoutes().forEach(r ->{String alias = aliasForService(r.getLocation()); serviceAliases.put(r.getLocation(), alias); metrics.counter(alias)})} private String aliasForService(String name){return String.format("metrics.%s.requestCount", name)} @Override public boolean shouldFilter(){return true} @Override public int filterOrder(){return 10} @Override public String filterType(){return "pre"} @Override public Object run(){RequestContext ctx = RequestContext.getCurrentContext(); metrics.counter(serviceAliases.get((String)ctx.get("serviceId"))).inc(); return null} } - And we configure it so that all requests must be prefixed with
/api. Also we want to disable every service registered with Eureka except ourcf-demo-app. We can use a different name for our service in the URL. Instead of/api/demo/but use/api/demo-service.
zuul: prefix: /api ignored-services: '*' routes: demo: /demo-service/** - Invoke the service several times (eg.
http://localhost:8082/api/demo-service/hello?name=Bob) and check the metrics using the metrics endpointhttp://localhost:8082/metrics | jq '.["metrics.demo.requestCount"]'.