Estimated Time: 25 minutes
1. Preface
Route services can be put to use in many ways: to cache responses from back-end services, to control access to an application, to audit how an application is accessed, and more.
In this lab you’ll deploy a route service that will allow you to rate-limit access to the attendee-service application. In the process you’ll learn yet more cf CLI commands, namely the create-user-provided-service
command with a new flag (-r
), and the bind-route-service
command.
Don’t forget to examine the source code to understand how this route service is implemented.
2. Exercises
2.1. Setup
-
Download this zip file, which contains source code and jar ready for you to deploy (no building necessary). Copy the file to folder:
~/pivotal-cloud-foundry-developer-workshop/
(~
is shorthand for the home directory in Linux, Mac and Unix based operating systems). You will need to create this directory in your home directory. -
Extract the the zip file to
~/pivotal-cloud-foundry-developer-workshop/route-service
. -
OPTIONAL STEP - Import applications into your IDE such as Spring Tool Suite (STS).
STS Import Help:
Select
Then select Maven → Existing Maven Projects. On the Import Maven Project page, browse to the directory where you extracted the zip. Then push the "Next" button. Click "Finish".2.2. Route Service Overview
-
Review the documentation on Route Services.
2.3. Scenario
Route services can be used for a number of things such as logging, transformations, security and rate limiting.
Our rate-limiter-app
application will do a couple of things. It will log incoming and outgoing requests. It will also impose a rate limit. No more than 3 requests per 15 seconds. Rate limited requests will be returned with a HTTP status code 429 (too many requests). Rate limiting is very common in the API space. Rate limiting protects your API from being overrun. The rate-limiter-app
application will keep its state in Redis.
The attendee-service
service exposes a RESTful API, so we will front it with our rate-limiter-app
.
2.4. Implementing rate-limiter-app
-
Review the following file:
~/pivotal-cloud-foundry-developer-workshop/route-service/src/main/java/org/cloudfoundry/example/Controller.java
.@RestController final class Controller { static final String FORWARDED_URL = "X-CF-Forwarded-Url"; static final String PROXY_METADATA = "X-CF-Proxy-Metadata"; static final String PROXY_SIGNATURE = "X-CF-Proxy-Signature"; private final static Logger logger = LoggerFactory.getLogger(Controller.class); private final RestOperations restOperations; private RateLimiter rateLimiter; @Autowired Controller(RestOperations restOperations, RateLimiter rateLimiter) { this.restOperations = restOperations; this.rateLimiter = rateLimiter; } @RequestMapping(headers = {FORWARDED_URL, PROXY_METADATA, PROXY_SIGNATURE}) ResponseEntity<?> service(RequestEntity<byte[]> incoming) { logger.debug("Incoming Request: {}", incoming); if(rateLimiter.rateLimitRequest(incoming)){ logger.debug("Rate Limit imposed"); return new ResponseEntity<>(HttpStatus.TOO_MANY_REQUESTS); }; RequestEntity<?> outgoing = getOutgoingRequest(incoming); logger.debug("Outgoing Request: {}", outgoing); return this.restOperations.exchange(outgoing, byte[].class); } private static RequestEntity<?> getOutgoingRequest(RequestEntity<?> incoming) { HttpHeaders headers = new HttpHeaders(); headers.putAll(incoming.getHeaders()); URI uri = headers.remove(FORWARDED_URL).stream() .findFirst() .map(URI::create) .orElseThrow(() -> new IllegalStateException(String.format("No %s header present", FORWARDED_URL))); return new RequestEntity<>(incoming.getBody(), headers, incoming.getMethod(), uri); } }
-
Review the following file:
~/pivotal-cloud-foundry-developer-workshop/route-service/src/main/java/org/cloudfoundry/example/RateLimiter.java
.@Component public class RateLimiter { private final static Logger logger = LoggerFactory.getLogger(RateLimiter.class); private final String KEY = "host"; @Autowired private StringRedisTemplate redisTemplate; @Scheduled(fixedRate = 15000) public void resetCounts() { redisTemplate.delete(KEY); logger.debug("Starting new 15 second interval"); } public boolean rateLimitRequest(RequestEntity<?> incoming) { String forwardUrl = incoming.getHeaders().get(Controller.FORWARDED_URL).get(0); URI uri; try { uri = new URI(forwardUrl); } catch (URISyntaxException e) { logger.error("error parsing url", e); return false; } String host = uri.getHost(); String value = (String)redisTemplate.opsForHash().get(KEY, host); int requestsPerInterval = 1; if (value == null){ redisTemplate.opsForHash().put(KEY, host, "1"); } else{ requestsPerInterval = Integer.parseInt(value) + 1; redisTemplate.opsForHash().increment(KEY, host, 1); } if(requestsPerInterval > 3) return true; else return false; } }
This is an example implementation for lab purposes only. A proper rate limiting service would need to uniquely identify the client. That can be accomplished via an API key, the X-Forwarded-For
header, or other approaches.
2.5. Push rate-limiter-app
-
Push
rate-limiter-app
:cd ~/pivotal-cloud-foundry-developer-workshop/route-service/
..and:
cf push rate-limiter-app -p ./target/route-service-1.0.0.BUILD-SNAPSHOT.jar -m 512M --random-route --no-start
-
Create a Redis service instance
In PWS, the marketplace service for Redis is called "rediscloud".
cf create-service rediscloud 30mb redis
Pivotal provides a redis managed service named "p-redis".
cf create-service p-redis shared-vm redis
-
Bind the service instance.
cf bind-service rate-limiter-app redis
-
Start the application.
cf start rate-limiter-app
2.6. Create a Route Service and Bind it to a Route
-
Create a user provided service. Let’s call it
rate-limiter-service
.cf create-user-provided-service rate-limiter-service -r {{ratelimiter_baseurl}}
-
Bind the
rate-limiter-service
to theattendee-service
route.cf bind-route-service {{domain_name}} rate-limiter-service --hostname {{attendee_service_hostname}}
2.7. Observe the effects of the rate-limiter-app
-
Tail the logs of the
rate-limiter-app
application.cf logs rate-limiter-app
-
Choose a client of your preference, but one that can show HTTP status code. Hit an
attendee-service
endpoint (e.g./attendees
) several times and see if you can get the rate limit to trigger. Observe the logs.Pic below is using Chrome with the Developer Tools.
2.8. Questions
-
What are the key headers used to implement route services (Service Instance Responsibilities)?
-
How would you apply route services in your environment?
2.9. Clean up
-
Unbind the route service.
cf unbind-route-service {{domain_name}} rate-limiter-service --hostname {{attendee_service_hostname}}
-
Delete
rate-limiter-service
service instance.cf delete-service rate-limiter-service
-
Unbind
redis
service instance from the app.cf unbind-service rate-limiter-app redis
-
Delete the
redis
service instance.cf delete-service redis
-
Delete the
rate-limiter-app
app.cf delete rate-limiter-app