Cloud Foundry Route Service Example
This project is an example of a Cloud Foundry Route Service written with Spring Bootb. This application does the following to each request:
- Intercepts an incoming request
- Logs information about that incoming request
- Transforms the incoming request to an outgoing request
- Logs information about that outgoing request
- Forwards the request and response
Requirements
Java, Maven
The application is written in Java 8 and packaged as a self executable JAR file. This enables it to run anywhere that Java is available.
Deployment
The following instructions assume that you have created an account and installed the
cf
command line tool.In order to automate the deployment process as much as possible, the project contains a Cloud Foundry manifest. To deploy run the following commands:
$ ./mvnw clean package
$ cf push
Next, create a user provided service that contains the route service configuration information. To do this, run the following command, substituting the address that the route service is listening on:
$ cf create-user-provided-service test-route-service -r https://<ROUTE-SERVICE-ADDRESS>
The next step assumes that you have an application already running that you'd like to bind this route service to. To do this, run the following command, substituting the domain and hostname bound to that application:
$ cf bind-route-service <APPLICATION-DOMAIN> test-route-service --hostname <APPLICATION-HOST>
In order to view the interception of the requests, you will need to stream the logs of the route service. To do this, run the following command:
$ cf logs route-service-example
Finally, start making requests against your test application. The route service's logs should start returning results that look similar to the following:
INFO Incoming Request: <PATCH http://localhost/route-service/patch,[B@4f453e63,{X-CF-Forwarded-Url=[http://localhost/original/patch], X-CF-Proxy-Metadata=[test-proxy-metadata], X-CF-Proxy-Signature=[test-proxy-signature], Content-Type=[text/plain], Content-Length=[9]}>
INFO Outgoing Request: <PATCH http://localhost/original/patch,[B@4f453e63,{X-CF-Proxy-Metadata=[test-proxy-metadata], X-CF-Proxy-Signature=[test-proxy-signature], Content-Type=[text/plain], Content-Length=[9]}>
INFO Incoming Request: <DELETE http://localhost/route-service/delete,{X-CF-Forwarded-Url=[http://localhost/original/delete], X-CF-Proxy-Metadata=[test-proxy-metadata], X-CF-Proxy-Signature=[test-proxy-signature]}>
INFO Outgoing Request: <DELETE http://localhost/original/delete,{X-CF-Proxy-Metadata=[test-proxy-metadata], X-CF-Proxy-Signature=[test-proxy-signature]}>
INFO Incoming Request: <HEAD http://localhost/route-service/head,{X-CF-Forwarded-Url=[http://localhost/original/head], X-CF-Proxy-Metadata=[test-proxy-metadata], X-CF-Proxy-Signature=[test-proxy-signature]}>
INFO Outgoing Request: <HEAD http://localhost/original/head,{X-CF-Proxy-Metadata=[test-proxy-metadata], X-CF-Proxy-Signature=[test-proxy-signature]}>
INFO Incoming Request: <PUT http://localhost/route-service/put,[B@12ffd1de,{X-CF-Forwarded-Url=[http://localhost/original/put], X-CF-Proxy-Metadata=[test-proxy-metadata], X-CF-Proxy-Signature=[test-proxy-signature], Content-Type=[text/plain], Content-Length=[9]}>
INFO Outgoing Request: <PUT http://localhost/original/put,[B@12ffd1de,{X-CF-Proxy-Metadata=[test-proxy-metadata], X-CF-Proxy-Signature=[test-proxy-signature], Content-Type=[text/plain], Content-Length=[9]}>
INFO Incoming Request: <POST http://localhost/route-service/post,[B@9d3c67,{X-CF-Forwarded-Url=[http://localhost/original/post], X-CF-Proxy-Metadata=[test-proxy-metadata], X-CF-Proxy-Signature=[test-proxy-signature], Content-Type=[text/plain], Content-Length=[9]}>
INFO Outgoing Request: <POST http://localhost/original/post,[B@9d3c67,{X-CF-Proxy-Metadata=[test-proxy-metadata], X-CF-Proxy-Signature=[test-proxy-signature], Content-Type=[text/plain], Content-Length=[9]}>
INFO Incoming Request: <GET http://localhost/route-service/get,{X-CF-Forwarded-Url=[http://localhost/original/get], X-CF-Proxy-Metadata=[test-proxy-metadata], X-CF-Proxy-Signature=[test-proxy-signature]}>
INFO Outgoing Request: <GET http://localhost/original/get,{X-CF-Proxy-Metadata=[test-proxy-metadata], X-CF-Proxy-Signature=[test-proxy-signature]}>
Developing
The project is set up as a Maven project and doesn't have any special requirements beyond that. It has been created using IntelliJ and contains configuration information for that environment, but should work with other IDEs.
https://docs.pivotal.io/pivotalcf/1-11/services/route-services.html
CLI to create route service
============================
cf create-user-provided-service SERVICE_INSTANCE [-p CREDENTIALS] [-l SYSLOG_DRAIN_URL] [-r ROUTE_SERVICE_URL]
e.g. cf create-user-provided-service my-route-service -r https://route-service-88.cfapps.io
e.g. cf cups my-route-service -r https://route-service-88.cfapps.io
CLI to bind service to route:
=============================
cf bind-route-service DOMAIN SERVICE_INSTANCE [-n HOST] [-f]
cf bind-route-service cfapps.io my-route-service --hostname testapp-88
CLI to unbind service to route:
==============================
cf unbind-route-service DOMAIN SERVICE_INSTANCE [-n HOST] [-f]
cf unbind-route-service cfapps.io my-route-service --hostname testapp-88
Pivotal Cloud Foundry Developer
Route Service
Customer agrees that the terms and conditions set forth here and here are incorporated by reference into this Data Sheet and shall govern the provision of Pivotal’s Services herein.
Customer may not record the training in any medium. Customer may not reproduce/copy, nor distribute/share the training materials in any capacity.
Purpose of this lab
How to create a route service
Estimated Time: 25 minutes
Exercises
Setup
1. Download the zip file. The zip file 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.
2. Extract the the zip file to ~/pivotal-cloud-foundry-developer-workshop/route-service .
3. OPTIONAL STEP - Import applications into your IDE such as Spring Tool Suite (STS).
STS Import Help:
Select File → Import… 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".
Route Service Overview
1. Review the documentation on Route Services.
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-limiterapp
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 .
Implementing rate-limiter-app
1. Review the following file: ~/pivotal-cloud-foundry-developer-workshop/routeservice/
src/main/java/org/cloudfoundry/example/Controller.java .
What's hap p ening ?
The service method is where the rate-limiter-app application handles incoming requests.
1. Any request with the X-CF-Forwarded-Url , X-CF-Proxy-Metadata , and X-CF-Proxy-Signature headers gets handled by
the service method.
2. Log the incoming request.
3. Check the rateLimiter to see if the number of requests has exceeded the rate limit threshold. If the threshold is exceeded
return a HTTP status code 429 (too many requests). If the threshold is not exceeded remove the FORWARDED_URL header, log
the outgoing request, and send the outgoing request to the downstream application.
@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);
}
}
2. Review the following file: ~/pivotal-cloud-foundry-developer-workshop/routeservice/
src/main/java/org/cloudfoundry/example/RateLimiter.java .
What's hap p ening ?
The rateLimitRequest method determines whether a request should be rate limited.
1. Increment the request count by host.
2. Return true if request should be rate limited ( requestsPerInterval > 3 ).
3. Return false if request should not be rate limited ( requestsPerInterval <= 3 ).
The resetCounts method deletes the Redis KEY every 15 seconds, which resets the counts by deleting all the state.
NOTE: 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.
Push rate-limiter-app
1. Push rate-limiter-app .
@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);
}
return requestsPerInterval > 3;
}
}
2. Create a Redis service instance.
Pivotal Cloud Foundry:
Pivotal Web Services:
3. Bind the service instance.
4. Start the application.
Create a Route Service and Bind it to a Route
1. Create a user provided service. Let's call it rate-limiter-service .
For Example:
2. Bind the rate-limiter-service to the attendee-service route.
For Example:
Observe the effects of the rate-limiter-app
1. Tail the logs of the rate-limiter-app application.
2. 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.
cd ~/pivotal-cloud-foundry-developer-workshop/route-service/
cf push rate-limiter-app -p ./target/route-service-1.0.0.BUILD-SNAPSHOT.jar -m 512M --randomroute
--no-start
cf create-service p-redis shared-vm redis
cf create-service rediscloud 30mb redis
cf bind-service rate-limiter-app redis
cf start rate-limiter-app
cf create-user-provided-service rate-limiter-service -r https://<RATE-LIMITER-APP-ADDRESS>
cf create-user-provided-service rate-limiter-service -r https://route-service-random-route.cf
apps.io
cf bind-route-service <APPLICATION-DOMAIN> rate-limiter-service --hostname <APPLICATION-HOST>
cf bind-route-service cfapps.io rate-limiter-service --hostname attendee-service-random-route
cf logs rate-limiter-app
Pic below is using Chrome with the Developer Tools.
Questions
What are the key headers used to implement route services (Service Instance Responsibilities)?
How would you apply route services in your environment?
Clean up
1. Unbind the route service.
For Example:
2. Delete rate-limiter-service service instance.
3. Unbind redis service instance from the app.
4. Delete the redis service instance.
5. Delete the rate-limiter-app app.
cf unbind-route-service <APPLICATION-DOMAIN> rate-limiter-service --hostname <APPLICATION-HOS
T>
cf unbind-route-service cfapps.io rate-limiter-service --hostname attendee-service-random-rou
te
cf delete-service rate-limiter-service
cf unbind-service rate-limiter-app redis
cf delete-service redis
cf delete rate-limiter-app
$ cf logs ratelimiter
14:16:20.49-0600 [App/0] OUT request from [10.244.0.25]
14:16:20.49-0600 [App/0] OUT request from [10.244.0.25]
14:16:20.49-0600 [App/0] OUT request from [10.244.0.25]
14:16:20.49-0600 [App/0] OUT request from [10.244.0.25]
14:16:20.49-0600 [App/0] OUT request from [10.244.0.25]
14:16:20.49-0600 [App/0] OUT request from [10.244.0.25]
14:16:20.49-0600 [App/0] OUT request from [10.244.0.25]
14:16:20.49-0600 [App/0] OUT request from [10.244.0.25]
14:16:20.49-0600 [App/0] OUT request from [10.244.0.25]
14:16:20.49-0600 [App/0] OUT request from [10.244.0.25]
14:16:20.50-0600 [RTR/0] OUT ratelimiter.bosh-lite.com - [20/02/2016:20:16:20 +0000] "GET / HTTP/1.1" 200 0 25 "-" "Go-http-client/1.1" 10.244.0.21:38101 x_forwarded_for:"192.168.50.1, 10.244.0.21" x_forwarded_proto:"http" vcap_request_id:127716be-9cd7-484d-634c-d1051993accb response_time:0.013694649 app_id:7a1745bc-d7cb-43a3-8201-c6ac5d75e79c
14:16:20.51-0600 [RTR/0] OUT ratelimiter.bosh-lite.com - [20/02/2016:20:16:20 +0000] "GET / HTTP/1.1" 200 0 25 "-" "Go-http-client/1.1" 10.244.0.21:38102 x_forwarded_for:"192.168.50.1, 10.244.0.21" x_forwarded_proto:"http" vcap_request_id:cf60dc83-bff1-4e53-6ff7-43a52667e2a4 response_time:0.017450561 app_id:7a1745bc-d7cb-43a3-8201-c6ac5d75e79c
14:16:20.52-0600 [RTR/0] OUT ratelimiter.bosh-lite.com - [20/02/2016:20:16:20 +0000] "GET / HTTP/1.1" 200 0 25 "-" "Go-http-client/1.1" 10.244.0.21:38109 x_forwarded_for:"192.168.50.1, 10.244.0.21" x_forwarded_proto:"http" vcap_request_id:272a34cc-dcaa-4c13-5584-be28e25855a2 response_time:0.027553184 app_id:7a1745bc-d7cb-43a3-8201-c6ac5d75e79c
14:16:20.52-0600 [RTR/0] OUT ratelimiter.bosh-lite.com - [20/02/2016:20:16:20 +0000] "GET / HTTP/1.1" 200 0 25 "-" "Go-http-client/1.1" 10.244.0.21:38107 x_forwarded_for:"192.168.50.1, 10.244.0.21" x_forwarded_proto:"http" vcap_request_id:6c379238-d5fc-4fc5-7b27-e4776d830c41 response_time:0.029788514 app_id:7a1745bc-d7cb-43a3-8201-c6ac5d75e79c
14:16:20.53-0600 [RTR/0] OUT ratelimiter.bosh-lite.com - [20/02/2016:20:16:20 +0000] "GET / HTTP/1.1" 200 0 25 "-" "Go-http-client/1.1" 10.244.0.21:38106 x_forwarded_for:"192.168.50.1, 10.244.0.21" x_forwarded_proto:"http" vcap_request_id:2efdd43d-3429-4869-54f6-634cb4bc9854 response_time:0.038401721 app_id:7a1745bc-d7cb-43a3-8201-c6ac5d75e79c
14:16:20.53-0600 [RTR/0] OUT ratelimiter.bosh-lite.com - [20/02/2016:20:16:20 +0000] "GET / HTTP/1.1" 200 0 25 "-" "Go-http-client/1.1" 10.244.0.21:38103 x_forwarded_for:"192.168.50.1, 10.244.0.21" x_forwarded_proto:"http" vcap_request_id:2f059c95-31b9-42ab-4459-1fe0f54dc9fa response_time:0.038841171 app_id:7a1745bc-d7cb-43a3-8201-c6ac5d75e79c
14:16:20.54-0600 [RTR/0] OUT ratelimiter.bosh-lite.com - [20/02/2016:20:16:20 +0000] "GET / HTTP/1.1" 200 0 25 "-" "Go-http-client/1.1" 10.244.0.21:38108 x_forwarded_for:"192.168.50.1, 10.244.0.21" x_forwarded_proto:"http" vcap_request_id:950c9525-490a-4fd1-7b38-5f74514ea085 response_time:0.046893267 app_id:7a1745bc-d7cb-43a3-8201-c6ac5d75e79c
14:16:20.55-0600 [RTR/0] OUT ratelimiter.bosh-lite.com - [20/02/2016:20:16:20 +0000] "GET / HTTP/1.1" 200 0 25 "-" "Go-http-client/1.1" 10.244.0.21:38104 x_forwarded_for:"192.168.50.1, 10.244.0.21" x_forwarded_proto:"http" vcap_request_id:49c5650b-c511-4fe5-7f56-067e404d7b8b response_time:0.059563104 app_id:7a1745bc-d7cb-43a3-8201-c6ac5d75e79c
14:16:20.55-0600 [RTR/0] OUT ratelimiter.bosh-lite.com - [20/02/2016:20:16:20 +0000] "GET / HTTP/1.1" 200 0 25 "-" "Go-http-client/1.1" 10.244.0.21:38105 x_forwarded_for:"192.168.50.1, 10.244.0.21" x_forwarded_proto:"http" vcap_request_id:de96c743-032f-4977-4466-f9241a03cf6a response_time:0.063431108 app_id:7a1745bc-d7cb-43a3-8201-c6ac5d75e79c
14:16:20.56-0600 [RTR/0] OUT ratelimiter.bosh-lite.com - [20/02/2016:20:16:20 +0000] "GET / HTTP/1.1" 200 0 25 "-" "Go-http-client/1.1" 10.244.0.21:38111 x_forwarded_for:"192.168.50.1, 10.244.0.21" x_forwarded_proto:"http" vcap_request_id:081cb579-a038-4511-43e9-b8f2074019b9 response_time:0.070817607 app_id:7a1745bc-d7cb-43a3-8201-c6ac5d75e79c
14:16:20.64-0600 [App/0] OUT request from [10.244.0.25]
14:16:20.64-0600 [App/0] OUT request from [10.244.0.25]
14:16:20.64-0600 [App/0] OUT rate limit exceeded for 10.244.0.25
14:16:20.64-0600 [RTR/0] OUT ratelimiter.bosh-lite.com - [20/02/2016:20:16:20 +0000] "GET / HTTP/1.1" 429 0 17 "-" "Go-http-client/1.1" 10.244.0.21:38144 x_forwarded_for:
No comments:
Post a Comment