Skip to content

Latest commit

 

History

History
550 lines (434 loc) · 27.1 KB

README.md

File metadata and controls

550 lines (434 loc) · 27.1 KB

PCF Developers workshop

Introduction

git clone https://github.com/MarcialRosales/java-pcf-workshops.git

Pivotal Cloud Foundry Technical Overview

Reference documentation:

Run Spring boot app

We have a spring boot application which provides a list of available flights based on some origin and destination.

  1. git fetch (git branch -a lists all the remote branches e.g origin/load-flights-from-in-memory-db)
  2. git checkout load-flights-from-in-memory-db
  3. cd java-pcf-workshops/apps/flight-availability
  4. mvn spring-boot:run
  5. curl 'localhost:8080?origin=MAD&destination=FRA'

We would like to make this application available to our clients. How would you do it today?

Deploying simple apps

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 a Spring boot app

Deploy flight availability and make it publicly available on a given public domain

  1. git checkout load-flights-from-in-memory-db
  2. cd java-pcf-workshops/apps/flight-availability
  3. Build the app
    mvn install
  4. Deploy the app
    cf push flight-availability -p target/flight-availability-0.0.1-SNAPSHOT.jar --random-route
  5. Try to deploy the application using a manifest
  6. Check out application's details, whats the url?
    cf app flight-availability
  7. Check out the health of the application (thanks to the actuator) /health endpoint:
    curl <url>/health
  8. Check out the environment variables of the application (thanks to the actuator) /env endpoint:
    curl <url>/env

Deploy a web site

Deploy Maven site associated to the flight availability and make it internally available on a given private domain

  1. git checkout load-flights-from-in-memory-db
  2. cd java-pcf-workshops/apps/flight-availability
  3. 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
  4. Deploy the app
    cf push flight-availability-site -p target/site --random-route use this command if you build it cf push flight-availability-site -p site --random-route use this command if you are pushing the already built site
  5. Check out application's details, whats the url?
    cf app flight-availability-site

Deploying applications with application manifest

  • 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 app
    • env: CF_STAGING_TIMEOUT (15min) Max wait time for buildpack staging, in minutes
    • env: 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)

Cloud Foundry services

Load flights from a provisioned database

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.

  1. git checkout load-flights-from-db
  2. cd apps/flight-availability
  3. Run the app
    mvn spring-boot:run
  4. Test it
    curl 'localhost:8080?origin=MAD&destination=FRA' shall return [{"id":2,"origin":"MAD","destination":"FRA"}]
  5. 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?

  1. 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

  1. Check out the database credentials the application is using:
    cf env flight-availability

  2. Test the application. Whats the url?

  3. We did not include any jdbc drivers with the application. How could that work?

Load flights fares from an external application

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.

  1. git checkout load-fares-from-external-app
  2. cd apps/flight-availability (on terminal 1)
  3. Run the flight-availability app mvn spring-boot:run
  4. cd apps/fare-service (on terminal 2)
  5. Run the fare-service apps
    mvn spring-boot:run
  6. 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.

  1. 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>
    
  2. 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"``

  3. Inject credentials using a User Provided Service. We are going to tackle this step in a separate lab.

Load flights fares from an external application using User Provided Services

Reference documentation:

  1. 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>" }'

  2. Add fare-service as a service to the flight-availability manifest.yml

      ...
    	services:
    	- flight-repository
    	- fare-service
    

    When we push the flight-availability, PCF will inject the fare-service credentials to the VCAP_SERVICES environment variable.

  3. Create a brand new project called cloud-services where we extend the Spring Cloud Connectors. This project is able to parse VCAP_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>  
    
    
  4. 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.

  5. 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 from CloudFoundryServiceInfoCreator and rely on the name of the service starting with a prefix like "ws-" for instance "ws-fare-service".

  6. 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
    
  7. 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();
    
    	}
    }
    
  8. Build and push the flight-availability service

  9. Test it curl 'https://<my flight availability app>/fares?origin=MAD&destination=FRA'

  10. Maybe it fails ...

  11. 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*.

Let external application access a platform provided service

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.

  1. Create a service instance
  2. Create a service key cf create-service-key <serviceInstanceName> <ourServiceKeyName>
  3. 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.

Routes and Domains

Reference documentation:

Private and Public routes/domains (internal vs external facing applications)

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).

Blue-Green deployment

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

Routing Services (intercept every request to decide whether to accept it or enrich it or track it)

Reference documentation:

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.

Create the Proxy (or router service)

Lab: The code is already provided in the routes branch however we are going to walk thru the code below:

  1. Create a Spring Boot application with a web dependency
  <dependency>
  		<groupId>org.springframework.boot</groupId>
  		<artifactId>spring-boot-starter-web</artifactId>
  	</dependency>
  1. 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;
    	}
    
    }
  2. 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);
  }

}
  1. 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
    }
  2. Forward request to the uri in X-CF-Forwarded-Url along with the other 2 headers X-CF-Proxy-Metadata and X-CF-Proxy-Signature. We remove the Authorization 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);
    }
  3. Build the app mvn install

Test the proxy locally

To test it locally we proceed as follow:

  1. Run the previous flight-availability app (assume that it is running on 8080)
  2. 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
  3. 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

Test the proxy in Cloud Foundry using Router Service functionality

Let's deploy it to Cloud Foundry.

  1. cf push -f target/manifest.yml
  2. Create a user provided service that points to the url of our deployed route-service.
cf cups route-service -r https://<route-service url>
  1. Deploy the flight-availability app if it is not already deployed:

  2. Configure Cloud Foundry to intercept all requests for flight-availability with the router service route-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
  1. Run in a terminal cf logs route-service to watch its logs
  2. 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.