Technical Articles
How To Test CloudFoundry Apps..outside of CloudFoundry
Background
A while ago I wrote a blog on the importance of using test environments which are reliable to set up and centrally managed. Building on the concepts laid out in that post, I set out to find a toolbox which would enable me to run an entire landscape of CloudFoundry apps on any infrastructure. My initial choice of technology (using vagrant to provide a VM with all required assets) turned out to be rather rigid and difficult to work with. So I decided to not reinvent the wheel and instead build on Docker, which is nowadays more mainstream & flexible at the same time.
In conclusion this blog will provide a walkthrough for using Docker to run a set of CloudFoundry apps in a test environment outside of CloudFoundry. In other words we will go from here:
to here:
The apps in this sample use a postgres DB as a backing service, while security & the Fiori Launchpad are provided by the “trio” appRouter/xsuaa/portal. This architectural setup should be rather common on the SAP Cloud Platform, being derived from the well-known HANA XSA Programming Model.
Disclaimer: Before we move on, I want to emphasize that this is a test environment. It is designed to be used for functional tests during development, not as a replacement for your productive environment.
In order to be able to run apps outside of CloudFoundry, it is important to first understand how they run inside of it
Understanding CloudFoundry
Containerization
Whenever you push some code to CloudFoundry, one of its buildpacks will package the code in a so called droplet, i.e. a ready-to-be-executed archive containing all necessary dependencies, libraries etc.
Platform Services & the App Environment
The CloudFoundry platform interacts with an app mainly through its environment: whenever you attach a backing service(database, xsuaa etc.) to an app, CloudFoundry will simply provide information for accessing this service in the environment of the app.
Networking
On upload, apps on CloudFoundry get a unique (public) hostname through which they can be addressed. When running multiple instances of an app, the platform will automatically load balance requests across these instances.
Moving outside of CloudFoundry
Getting an app to run outside of CloudFoundry requires addressing all of the above 3 aspects:
Containerization
To have a running test environment, we need to containerize and run:
- our own applications
- the SAP Fiori Launchpad
- any services that we decide to host ourself (in this case the postgres DB)
e.g. a Spring Boot app can be dockerized with a dockerfile like the following:
FROM openjdk:8-jdk-alpine
ARG JAR_FILE
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java","-jar","/app.jar"]
If you’re new to Docker, here are some more resources to get you started:
- https://www.baeldung.com/dockerizing-spring-boot-application
- https://nodejs.org/en/docs/guides/nodejs-docker-webapp/
This should be good enough for most cases. However, if you are looking for more dev-prod parity, you can use a plugin like cf local to create a docker image using the exact same buildpack that CloudFoundry would use. This is particularly important if you rely on the buildpack to autoconfigure your app (that’s not in the scope of this blog though)
Platform Services & the App Environment
Enabling your apps to run in a CF-like environment basically means providing environment variables which point to valid backing services.
Note: environment variables contain confidential information. Keep them safe and do not check them into version control.
Depending on the type of service that you are trying to consume, you will run into one of 2 scenarios. For some services its easiest to just spin up your own service instance and run your apps against it. In our sample project this is what we’ll do with the postgres DB. In other cases you might not be able to run you own instance, so you’ll have to consume the instance running in CloudFoundry. In our example we will use this approach with the authentication(xsuaa) and Portal service.
To run an app against a service instance running in CloudFoundry you need to retrieve the credentials and provide them to the app environment. Doing this manually for an xsuaa instance would require the following:
- create and retrieve a service key
cf create-service-key xsuaa_instance local_test
cf service-key xsuaa_instance local_test > xsuaa.credentials.json
- prepare the VCAP_SERVICES variable using the xsuaa credentials
"VCAP_SERVICES": {
"xsuaa": [
{
"binding_name": null,
"credentials": {
// .. xsuaa.crendetials.json content goes here
},
"instance_name": "myuaa",
"label": "xsuaa",
"name": "myuaa",
"plan": "application",
"provider": null,
"syslog_drain_url": null,
"tags": [
"xsuaa"
],
"volume_mounts": []
}
]
}
When instead running against your own service instance you have to manually provide the service url & credentials.
"credentials": {
"dbname": "mydb",
"end_points": [
{
"host": "mypostgres",
"network_id": "SF",
"port": "5432"
}
],
"hostname": "localhost",
"password": "pwd",
"port": "5432",
"ports": {
"5432/tcp": "5432"
},
"uri": "postgres://postgres:pwd@mypostgres:5432/mydb",
"username": "postgres"
}
In this example the postgres instance will be accessible using the alias “mypostgres”. How exactly your app & services communicate with each other depends on your networking setup (see below).
These are fundamentally the steps you would have to do if you were doing things manually. However, managing multiple apps manually this way can be very tedious and error prone. The last chapter describes a more efficient approach to handling this situation. Keep reading.
Networking
Apps on CloudFoundry are publicly accessible through their hostname. The same is not true by default when running an app in a Docker container. Docker provides several networking options, but whenever possible it’s easiest to stick with the basics.
Our architecture imposes the following requirements on our network setup:
1. Apps and self-hosted services must be able to communicate with each other
As a result we attach all containers to the same virtual network. This way they can address each other using the container name as hostname
2. Some containers require internet access
Works out of the box, but remember to set your proxy configuration when your corporate network requires it.
3. The Fiori Launchpad must be “externally” accessible
We must enable port-forwarding from the docker host to the Fiori Launchpad container, to make the Launchpad “externally” accessible (i.e. from outside of the Docker network)
Putting the pieces together
The puzzle now consists of multiple pieces that need to be put together:
- several docker images
- environment variables for every container
- a Docker network and port forwarding for the application router
A good way to define all these centrally is by using docker-compose. Everything discussed in this blog would be summarized in a docker-compose file like this:
version: '3'
services:
launchpad:
build: ../src/launchpad
container_name: launchpad
ports:
- "8080:8080"
networks:
- NET1
environment:
- PORT=8080
- VCAP_SERVICES={"portal-services":[{"binding_name":null,"credentials":{...
- DESTINATIONS=[{"name":"app1","url":"http://spring-app1","forwardAuthToken":true},...
- sapui5url=https://sapui5.hana.ondemand.com/1.56.10
app1:
build: ../src/app
container_name: spring-app1
networks:
- NET1
environment:
- SPRING_PROFILES_ACTIVE=cloud
- VCAP_APPLICATION={}
- VCAP_SERVICES={"postgresql":[{..."host":"mypostgres"..
app2:
build: ../src/app2
container_name: spring-app2
networks:
- NET1
environment:
- SPRING_PROFILES_ACTIVE=cloud
- VCAP_APPLICATION={}
- VCAP_SERVICES={"postgresql":[{..."host":"mypostgres"..
postgres:
image: "postgres"
container_name: mypostgres
networks:
- NET1
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
A simple “docker-compose up” is then sufficient to start up your entire landscape, and you’re ready to go.
However, I find maintaining this kind of yaml file manually rather painful. Moreover, since the file contains confidential information, it’s important to keep it out of version control and thus be able to generate it on-demand.
Luckily, with a bit of glue code everything can be set up automatically for you. Want to know how? Check out the SAP CP CF Starter Project: https://github.com/ionescuv/sap-cp-cf-starter
The starter project provides a toolkit which:
- Sets up the necessary services in your SAP CP Cloud Foundry account
- Retrieves services credentials and generates the docker-compose.yml for you
- Builds & runs docker images for all apps and dependencies
Give it a try and let me know what you think!
Very nice, thanks for sharing Victor!