- Introduction
- Pivotal Cloud Foundry Technical Overview
- Deploying simple apps
- Cloud Foundry services
- Routes and Domains
- Orgs, Spaces and Users
- Build packs
git clone https://github.com/MarcialRosales/java-pcf-workshops.git
Reference documentation:
We have a spring boot application which provides a list of available flights based on some origin and destination.
git fetch
(git branch -a
lists all the remote branches e.gorigin/load-flights-from-in-memory-db
)git checkout load-flights-from-in-memory-db
cd java-pcf-workshops/apps/flight-availability
mvn spring-boot:run
curl 'localhost:8080?origin=MAD&destination=FRA'
We would like to make this application available to our clients. How would you do it today?
CloudFoundry excels at the developer experience: deploy, update and scale applications on-demand regardless of the application stack (java, php, node.js, go, etc). We are going to learn how to deploy 4 types of applications: java, static web pages, php and .net applications without writing any logic/script to make it happen.
Reference documentation:
Deploy flight availability and make it publicly available on a given public domain
git checkout load-flights-from-in-memory-db
cd java-pcf-workshops/apps/flight-availability
- Build the app
mvn install
- Deploy the app
cf push flight-availability -p target/flight-availability-0.0.1-SNAPSHOT.jar --random-route
- Try to deploy the application using a manifest
- Check out application's details, whats the url?
cf app flight-availability
- Check out the health of the application (thanks to the actuator)
/health
endpoint:
curl <url>/health
- Check out the environment variables of the application (thanks to the actuator)
/env
endpoint:
curl <url>/env
Deploy Maven site associated to the flight availability and make it internally available on a given private domain
git checkout load-flights-from-in-memory-db
cd java-pcf-workshops/apps/flight-availability
- Build the site. Maven literally downloads hundreds of jars to generate the maven site with all the project reports such as javadoc, sure-fire reports, and others. For this reason, there is a
site
folder which has an already site. If you have a good internet connection, try this command instead:mvn site
- Deploy the app
cf push flight-availability-site -p target/site --random-route
use this command if you build itcf push flight-availability-site -p site --random-route
use this command if you are pushing the already built site - Check out application's details, whats the url?
cf app flight-availability-site
- simplify push command with manifest files (
-f <manifest>
,-no-manifest
) - register applications with DNS (
domain
,domains
,host
,hosts
,no-hostname
,random-route
,routes
). We can register http and tcp endpoints. - deploy applications without registering with DNS (
no-route
) (for instance, a messaging based server which does not listen on any port) - specify compute resources : memory size, disk size and number of instances!! (Use manifest to store the 'default' number of instances ) (
instances
,disk_quota
,memory
) - specify environment variables the application needs (
env
) - as far as CloudFoundry is concerned, it is important that application start (and shutdown) quickly. If we are application is too slow we can adjust the timeouts CloudFoundry uses before it deems an application as failed and it restarts it:
timeout
(60sec) Time (in seconds) allowed to elapse between starting up an app and the first healthy response from the appenv: CF_STAGING_TIMEOUT
(15min) Max wait time for buildpack staging, in minutesenv: CF_STARTUP_TIMEOUT
(5min) Max wait time for app instance startup, in minutes
- CloudFoundry is able to determine the health status of an application and restart if it is not healthy. We can tell it not to check or to checking the port (80) is opened or whether the http endpoint returns a
200 OK
(health-check-http-endpoint
,health-check-type
) - CloudFoundry builds images from our applications. It uses a set of scripts to build images called buildpacks. There are buildpacks for different type of applications. CloudFoundry will automatically detect the type of application however we can tell CloudFoundry which buildpack we want to use. (
buildpack
) - specify services the application needs (
services
)
We want to load the flights from a relational database (mysql) provisioned by the platform not an in-memory database. We are implementing the FlightService
interface so that we can load them from a FlightRepository
. We need to convert Flight
to a JPA Entity. We added hsqldb a runtime dependency so that we can run it locally.
git checkout load-flights-from-db
cd apps/flight-availability
- Run the app
mvn spring-boot:run
- Test it
curl 'localhost:8080?origin=MAD&destination=FRA'
shall return[{"id":2,"origin":"MAD","destination":"FRA"}]
- Before we deploy our application to PCF we need to provision a mysql database. If we tried to push the application without creating the service we get:
... FAILED Could not find service flight-repository to bind to mr-fa
cf marketplace
Check out what services are available
cf marketplace -s p-mysql pre-existing-plan ...
Check out the service details like available plans
cf create-service ...
Create a service instance with the name flight-repository
cf service ...
Check out the service instance. Is it ready to use?
- Push the application using the manifest. See the manifest and observe we have declared a service:
applications:
- name: flight-availability
instances: 1
memory: 1024M
path: @project.build.finalName@[email protected]@
random-route: true
services:
- flight-repository
-
Check out the database credentials the application is using:
cf env flight-availability
-
Test the application. Whats the url?
-
We did not include any jdbc drivers with the application. How could that work?
We want to load the flights from a relational database and the prices from an external application. For the sake of this exercise, we are going to mock up the external application in cloud foundry.
git checkout load-fares-from-external-app
cd apps/flight-availability
(on terminal 1)- Run the flight-availability app
mvn spring-boot:run
cd apps/fare-service
(on terminal 2)- Run the fare-service apps
mvn spring-boot:run
- Test it (on terminal 3)
curl 'localhost:8080/fares/origin=MAD&destination=FRA'
shall return something like this[{"fare":"0.016063185475725605","origin":"MAD","destination":"FRA","id":"2"}]
Let's have a look at the fare-service
. It is a pretty basic REST app configured with basic auth (Note: We could have simply relied on the spring security default configuration properties):
server.port: 8081
fare:
credentials:
user: user
password: password
And it simply returns a random fare for each requested flight:
@RestController
public class FareController {
private Random random = new Random(System.currentTimeMillis());
@PostMapping("/")
public String[] applyFares(@RequestBody Flight[] flights) {
return Arrays.stream(flights).map(f -> Double.toString(random.nextDouble())).toArray(String[]::new);
}
}
Let's have a look at how the flight-availability
talks to the fare-service
. First of all, the implementation of the FareService
interface uses RestTemplate
to call the Rest endpoint.
@Service
public class FareServiceImpl implements FareService {
private final RestTemplate restTemplate;
public FareServiceImpl(@Qualifier("fareService") RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
@Override
public String[] fares(Flight[] flights) {
return restTemplate.postForObject("/", flights, String[].class);
}
}
And we build the RestTemplate
specific for the FareService
(within FlightAvailabilityApplication.java
). See how we setup the RestTemplate with basic auth and the root uri for any requests to the fare-service
endpoint:
@Configuration
@ConfigurationProperties(prefix = "fare-service")
class FareServiceConfig {
String uri;
String username;
String password;
public String getUri() {
return uri;
}
public void setUri(String uri) {
this.uri = uri;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@Bean(name = "fareService")
public RestTemplate fareService(RestTemplateBuilder builder, FareServiceConfig fareService) {
return builder.basicAuthorization(getUsername(), getPassword()).rootUri(getUri()).build();
}
}
And we provide the credentials for the fare-service
in the application.yml
:
fare-service:
uri: http://localhost:8081
username: user
password: password
We tested it that it works locally. Now let's deploy to PCF. First we need to deploy fare-service
to PCF. Then we deploy flight-availability
service. Do we need to make any changes? We do need to configure the credentials to our fare-service.
We have several ways to configure the credentials for the fare-service
in flight-availability
.
-
Set credentials in application.yml, build the flight-availability app (
mvn install
) and push it (cf push <myapp> -f target/manifest.yml
).fare-service: uri: <copy the url of the fare-service in PCF>
-
Set credentials as environment variables in the manifest. Thanks to Spring boot configuration we can do something like this:
env: FARE_SERVICE_URI: http://<fare-service-uri> FARE_SERVICE_USERNAME: user FARE_SERVICE_PASSWORD: password
Rather than modifying the manifest again lets simly verify that this method works. Lets simply set a wrong username via command-line:
cf set-env <myapp> FARE_SERVICE_USERNAME "bob" cf env <myapp> (dont mind the cf restage warning message) cf restart <myapp>
And now test it,
curl 'https://mr-fa-cronk-iodism.apps-dev.chdc20-cf.solera.com/fares?origin=MAD&destination=FRA'
should return
`{"timestamp":1490776955527,"status":500,"error":"Internal Server Error","exception":"org.springframework.web.client.HttpClientErrorException","message":"401 Unauthorized","path":"/fares"`` -
Inject credentials using a User Provided Service. We are going to tackle this step in a separate lab.
Reference documentation:
- Spring Cloud Connectors
- Extending Spring Cloud Connectors
- Configuring Service Connections for Spring applications in Cloud Foundry
-
Create a User Provided Service which encapsulates the credentials we need to call the
fare-service
:
cf uups fare-service -p '{"uri": "https://user:password@<your-fare-service-uri>" }'
-
Add
fare-service
as a service to theflight-availability
manifest.yml... services: - flight-repository - fare-service
When we push the
flight-availability
, PCF will inject thefare-service
credentials to theVCAP_SERVICES
environment variable. -
Create a brand new project called
cloud-services
where we extend the Spring Cloud Connectors. This project is able to parseVCAP_SERVICES
and extract the credentials of standard services like relational database, RabbitMQ, Redis, etc. However we can extend it so that it can parse our custom service,fare-service
. This project can work with any cloud, not only CloudFoundry. However, given that we are working with Cloud Foundry we will add the implementation for Cloud Foundry:<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-cloudfoundry-connector</artifactId> <version>1.2.3.RELEASE</version> </dependency>
-
Create a ServiceInfo class that holds the credentials to access the
fare-service
. We are going to create a generic WebServiceInfo class that we can use to call any other web service. -
Create a ServiceInfoCreator class that creates an instance of ServiceInfo and populates it with the credentials exposed in
VCAP_SERVICES
. Our generic WebServiceInfoCreator. We are extending a class which provides most of the implementation. However, we cannot use it as is due to some limitations with the User Provided Services which does not allow us to tag our services. Instead, we need to set the tag within the credentials attribute. Another implementation could be to extend fromCloudFoundryServiceInfoCreator
and rely on the name of the service starting with a prefix like "ws-" for instance "ws-fare-service". -
Register our ServiceInfoCreator to the Spring Cloud Connectors framework by adding a file called org.springframework.cloud.cloudfoundry.CloudFoundryServiceInfoCreator with this content:
io.pivotal.demo.cups.cloud.cf.WebServiceInfoCreator
-
Provide 2 types of Configuration objects, one for Cloud and one for non-cloud (i.e. when running it locally). The Cloud one uses Spring Cloud Connectors to retrieve the
WebServiceInfo
object. First of all, we build a Cloud object and from this object we look up the WebServiceInfo and from it we build the RestTemplate.@Configuration @Profile({"cloud"}) class CloudConfig { @Bean Cloud cloud() { return new CloudFactory().getCloud(); } @Bean public WebServiceInfo fareServiceInfo(Cloud cloud) { ServiceInfo info = cloud.getServiceInfo("fare-service"); if (info instanceof WebServiceInfo) { return (WebServiceInfo)info; }else { throw new IllegalStateException("fare-service is not of type WebServiceInfo. Did you miss the tag attribute?"); } } @Bean(name = "fareService") public RestTemplate fareService(RestTemplateBuilder builder, WebServiceInfo fareService) { return builder.basicAuthorization(fareService.getUserName(), fareService.getPassword()).rootUri(fareService.getUri()).build(); } }
-
Build and push the
flight-availability
service -
Test it
curl 'https://<my flight availability app>/fares?origin=MAD&destination=FRA'
-
Maybe it fails ...
-
Maybe we had to declare the service like this:
cf uups fare-service -p '{"uri": "https://user:password@<your fare service uri>", "tag": "WebService" }'
Note in the logs the following statement: No suitable service info creator found for service fare-service Did you forget to add a ServiceInfoCreator?
. Spring Cloud Connectors can go one step further and create the ultimate application's service instance rather than only the ServiceInfo.
We leave to the attendee to modify the application so that it does not need to build a FareService Bean instead it is built via the Spring Cloud Connectors library.
- Create a FareServiceCreator class that extends from `AbstractServiceConnectorCreator<FareService, WebServiceInfo>`
- Register the FareServiceCreator in the file `org.springframework.cloud.service.ServiceConnectorCreator` under the `src/main/resources/META-INF/services` folder. Put the fully qualified name of your class in the file. e.g:
```
com.example.web.FareServiceCreator
```
- We don't need now the *Cloud* configuration class because the *Spring Cloud Connectors* will automatically create an instance of *FareService*.
Most likely, all the applications will run within the platform. However, if we ever had an external application access a service provided by the platform, say a database, there is a way to do it.
- Create a service instance
- Create a service key
cf create-service-key <serviceInstanceName> <ourServiceKeyName>
- Get the credentials
cf service-key <serviceInstanceName> <ourServiceKeyName>
. Share the credentials with the external application.
Creating a service-key is equivalent to binding an application to a service instance. The service broker creates a set of credentials for the application.
Reference documentation:
What domains exists in our organization? try cf domains
. Anyone is private? and what does it mean private? Private domain is that domain which is registered with the internal IP address of the load balancer. And additionally, this private domain is not registered with any public DNS name. In other words, there wont be any DNS server able to resolve the private domain.
The lab consists in leveraging private domains so that only internal applications are accessible within the platform. Lets use the fare-service
as an internal application.
There are various ways to implement this lab. One way is to actually declare the private domain in the application's manifest and redeploy it. Another way is to play directly with the route commands (create-route
, and delete-route
, map-route
, or unmap-route
).
Reference documentation:
Use the demo application to demonstrate how we can do blue-green deployments using what you have learnt so far with regards routes.
How would you do it? Say Blue is the current version which is running and green is the new version.
Key command: cf map-route
and cf unmap-route
Reference documentation:
- https://docs.pivotal.io/pivotalcf/services/route-services.html
- https://docs.pivotal.io/pivotalcf/devguide/services/route-binding.html
The purpose of the lab is to take any application and add a proxy layer that only accepts requests which carry a JWT Token else it fails with it a 401 Unauthorized
.
Reminder: Routing service is a mechanism that allows us to filter requests never to alter the original endpoint. We can reject the request or pass it on as it is or modified, e.g adding extra headers.
Lab: The code is already provided in the routes
branch however we are going to walk thru the code below:
- Create a Spring Boot application with a
web
dependency
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
- Create a @Controller class :
@RestController class RouteService { 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 Logger logger = LoggerFactory.getLogger(this.getClass()); private final RestOperations restOperations; @Autowired RouteService(RestOperations restOperations) { this.restOperations = restOperations; } }
- Add a single request handler that receives all requests:
@RequestMapping(headers = { FORWARDED_URL, PROXY_METADATA, PROXY_SIGNATURE })
ResponseEntity<?> service(RequestEntity<byte[]> incoming, @RequestHeader(name = "Authorization", required = false) String jwtToken ) {
if (jwtToken == null) {
this.logger.error("Incoming Request missing JWT Token: {}", incoming);
return badRequest();
}else if (!isValid(jwtToken)) {
this.logger.error("Incoming Request missing or not valid JWT Token: {}", incoming);
return notAuthorized();
}
RequestEntity<?> outgoing = getOutgoingRequest(incoming);
this.logger.debug("Outgoing Request: {}", outgoing);
return this.restOperations.exchange(outgoing, byte[].class);
}
}
- Validate JWT token header by simply checking that it starts with "Bearer". If it is not valid and/or it is missing, log it as an error.
private static boolean isValid(String jwtToken) { return jwtToken.contains("Bearer"); // TODO add JWT Validation }
- Forward request to the uri in
X-CF-Forwarded-Url
along with the other 2 headersX-CF-Proxy-Metadata
andX-CF-Proxy-Signature
. We remove theAuthorization
header as it is longer needed: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))); headers.remove("Authorization"); return new RequestEntity<>(incoming.getBody(), headers, incoming.getMethod(), uri); }
- Build the app
mvn install
To test it locally we proceed as follow:
- Run the previous flight-availability app (assume that it is running on 8080)
- Run this route-service app on port 8888 (or any other that you prefer) on a separate terminal :
mvn spring-boot:run -Dserver.port=8888
- Simulate request coming from a client via CF Router for url
http://localhost:8080
without any JWT token:
curl -v -H "X-CF-Forwarded-Url: http://localhost:8080/" -H "X-CF-Proxy-Metadata: some" -H "X-CF-Proxy-Signature: signature " localhost:8888/
We should get a 400 Bad Request
8. Simulate request coming from a client via CF Router for url http://localhost:8080
with invalid JWT token:
curl -v -H "X-CF-Forwarded-Url: http://localhost:8080" -H "X-CF-Proxy-Metadata: some" -H "X-CF-Proxy-Signature: signature " -H "Authorization: hello" localhost:8888/
We should get a 401 Unauthorized
9. Simulate request coming from a client via CF Router for url http://localhost:8080
with valid JWT token:
curl -v -H "X-CF-Forwarded-Url: http://localhost:8080" -H "X-CF-Proxy-Metadata: some" -H "X-CF-Proxy-Signature: signature " -H "Authorization: Bearer hello" localhost:8888/
We should get a 200 OK and the body hello
Let's deploy it to Cloud Foundry.
cf push -f target/manifest.yml
- Create a user provided service that points to the url of our deployed
route-service
.
cf cups route-service -r https://<route-service url>
-
Deploy the flight-availability app if it is not already deployed:
-
Configure Cloud Foundry to intercept all requests for
flight-availability
with the router serviceroute-service
:
cf bind-route-service <application_domain> route-service --hostname <app_hostname>
If you are not sure about the application_domain or app_hostname run: cf app flight-availability | grep urls
. It will be <app_hostname>.<application_domain>
5. Check that flight-availability
is bound to route-service
: cf routes
space host domain port path type apps service
development route-service-circulable-mistletoe apps-dev.chdc20-cf.xxxxxx.com route-service
development app1-sliceable-jerbil apps-dev.chdc20-cf.xxxxxx.com flight-availability route-service
- Run in a terminal
cf logs route-service
to watch its logs - Try a url which has no JWT token:
curl -v https://<app_hostname>.<application_domain>
We should get back a 400 Bad Request 7. Try a url which has an invalid JWT token:
curl -v -H "Authorization: hello" https://<app_hostname>.<application_domain>
We should get back a 401 Unauthorized 8. Finally, try a url which has a valid JWT Token:
curl -v -H "Authorization: Bearer hello" https://<app_hostname>.<application_domain>
We should get back a 200 OK and the outcome from the /
endpoint which is hello
.