Migrating Cloud Foundry to Azure Spring Apps for Java Developers

Jay Lee
10 min readAug 8, 2022

I can't help but say this to start an article. I still remember the day vividly when I was there in San Francisco watching Onsi's presentation on stage in Jan 2016, and it personally marks the start of my cloud native journey. Famous Cloud Foundry haiku presented by Onsi, "Here is my source code, run it on the cloud for me, I do not care how" totally fascinated me back then. This resonates so well even today that every day I see the struggles of developers learning Kubernetes. I believe Cloud Foundry haiku is the best vision that any PaaS would want to follow.

Cloud Foundry-like developer experience landed on Azure in 2020 when Microsoft and VMware announced a joint product, “Azure Spring Cloud” later renamed “Azure Spring Apps”. Azure Spring Apps has inherited many developer-friendly aspects from Cloud Foundry that even use buildpack internally(by kpack). Thanks to this joint product, Spring developers can use a first-party Azure service to enjoy similar PaaS experiences as Cloud Foundry.

This article demonstrates the differences between Cloud Foundry and Spring apps entirely from Cloud Foundry developers’ point of view so that this could be a good starting point for future migration. I wouldn’t cover the various feature exhaustively but rather focus on the things developers would use every day.

Spring music will be used for the demo — https://github.com/cloudfoundry-samples/spring-music

NOTE: This article is based on Tanzu Application Service(formerly Pivotal Cloud Foundry), however everything here would apply the same to any Cloud Foundry from IBM or SAP.

cf push

Cloud Foundry’s vision boils down to one powerful command, “cf push". This one command can handle a myriad of things like containerization of an app, ingress setup, service binding, limit and request, etc.

A common practice to supply arguments with cf push is to use the manifest.yml file, instead of giving lengthy options with CLI. Our sample app, Spring music has one as below.

$ git clone https://github.com/cloudfoundry-samples/spring-music.git
$ cd spring-music
$ cat manifest.yml
---
applications:
- name: spring-music
memory: 1G
random-route: true
path: build/libs/spring-music-1.0.jar
env:
JBP_CONFIG_SPRING_AUTO_RECONFIGURATION: '{enabled: false}'
SPRING_PROFILES_ACTIVE: http2

cf push always pick up the manifest.yml file by default. Yaml file pushes an app called spring-music with spring-music-1.0.jar, set the container memory limit to 1G, create a randomly generated DNS route, and set two environment variables.

$ ./gradlew clean build
$ cf push
Pushing app spring-music to org test-org / space dev as c**@gmail.com...
Applying manifest file /private/tmp/spring-music/manifest.yml...
Manifest applied
Packaging files to upload...
Uploading files...
49.83 MiB / 49.83 MiB [=================================================================================================] 100.00% 4s
Waiting for API to complete processing files...Staging app and tracing logs...
Downloading binary_buildpack...
Downloading r_buildpack...
Downloading php_buildpack...
Downloading ruby_buildpack...
Downloaded binary_buildpack
Downloading dotnet_core_buildpack...
Downloading nodejs_buildpack...
Downloaded ruby_buildpack
Downloading nginx_buildpack...
Downloaded nodejs_buildpack
...
Instances starting...
Instances starting...
Instances starting...
Instances starting...
Instances starting...
name: spring-music
requested state: started
routes: spring-music-grateful-pangolin-ck.apps.***
last uploaded: Sat 06 Aug 00:07:31 +08 2022
...

On Azure Spring Apps, the initial step is slightly different. Developers must create a placeholder first, which is required only once in the lifecycle.

$ az spring app create --name spring-music --service asc-standard --resource-group asc-rg --memory 1Gi --runtime-version Java_17 --assign-endpoint true
This command usually takes minutes to run. Add '--verbose' parameter if needed.
[1/3] Creating app spring-music
[2/3] Creating default deployment with name "default"
[3/3] Updating app "spring-music" (this operation can take a while to complete)
App create succeeded
{
"id": "/subscriptions/.../resourceGroups/asc-rg/providers/Microsoft.AppPlatform/Spring/asc-standard/apps/spring-music",
"identity": null,
"location": "eastus",
"name": "spring-music",
"properties": {
"activeDeployment": {
"id": "/subscriptions/.../resourceGroups/asc-rg/providers/Microsoft.AppPlatform/Spring/asc-standard/apps/spring-music/deployments/default",
"name": "default",
...
"provisioningState": "Succeeded",
"public": true,
"temporaryDisk": {
"mountPath": "/tmp",
"sizeInGb": 5
},
"url": "https://asc-standard-spring-music.azuremicroservices.io",
"vnetAddons": null
},
"resourceGroup": "asc-rg",
...
}

Cloud Foundry mainfest.yml above can be translated as below on ASA.

$ az spring app deploy -n spring-music -s asc-standard -g asc-rg --source-path . --env SPRING_PROFILES_ACTIVE=http2 JBP_CONFIG_SPRING_AUTO_RECONFIGURATION="'{enabled: false}'"
This command usually takes minutes to run. Add '--verbose' parameter if needed.
[1/3] Requesting for upload URL.
[2/3] Uploading package to blob.
[3/3] Updating deployment in app "spring-music" (this operation can take a while to complete)
...
Previous image with name "acr9788ecdca02b433e8.azurecr.io/720555919a4f415dbd087e25f5ed09f8-spring-music" not found
9 of 23 buildpacks participating
paketo-buildpacks/ca-certificates 3.2.3
paketo-buildpacks/microsoft-openjdk 2.2.5
paketo-buildpacks/syft 1.11.3
paketo-buildpacks/gradle 6.4.5
paketo-buildpacks/executable-jar 6.2.4
paketo-buildpacks/apache-tomcat 7.3.6
paketo-buildpacks/liberty 1.1.1
paketo-buildpacks/dist-zip 5.2.4
paketo-buildpacks/spring-boot 5.11.0
/ Running ..
...

Once the command is finished, open the browser and access the endpoint. We create an app with —-assign-endpoint true that creates the URL "https://asc-standard-spring-music.azuremicroservices.io". Open the browser and access it.

Spring Music running on Azure Spring Apps

There is a subtle difference between Cloud Foundry and ASA in terms of creating a route.

  • Cloud Foundry no-route — There is no equivalent on ASA. ASA always creates a test endpoint by default which is protected by HTTP Basic Auth.
  • Cloud Foundry random-route — There is no equivalent on ASA. ASA creates a URL based on the given app name.
  • Cloud Foundry map-route —Not Available on ASA
  • Cloud Foundry custom domain — ASA supports the custom domain

cf logs

cf logs by default show not only application logs but also platform logs including Stemcell, GoRouter, CAPI server, etc. And it by default tails the log.

$ cf logs spring-music

On Azure Spring Apps, az spring app logs only shows the application log, and the default mode is not tailing. Tailing requires -f option.

$ az spring app logs -g asc-rg -s asc-standard -n spring-music -f

cf logs --recent shows the recent logs.

$ cf logs spring-music --recent

You can see the logs for the specific instance using “--instance

$ az spring app logs -g asc-rg -s asc-standard -n spring-music$ az spring app logs -g asc-rg -s asc-standard -n spring-music -i spring-music-default-13-6747564bd8-4mqx2

Auto Scaling

Cloud Foundry provides an app autoscaler to auto-scale Application Instances by two methods, 1. metrics based and 2. schedule based. ASA supports two models exactly the same as Cloud Foundry, but There is one difference in the support of custom metrics. Cloud Foundry supports custom metrics emitted by the application code, but ASA doesn’t. However, ASA supports using JVM level metrics with the help of the Spring Boot Actuator. Aside from basic metrics like CPU, memory, network, etc, it also supports metrics like JVM GC pause or Tomcat total thread count.

ASA Metrics for autoscaling
Schedule based autoscaling

Marketplace and Service Binding

Cloud Foundry way of integrating third-party services is to use Service Instances. The concept of service binding is from the 12factor backing service. (https://12factor.net/backing-services). Any service created in a form of Service Instance can be bound or unbound to an app without changing the code. That is the realization of the 12-factor app. Cloud Foundry marketplace is to help developers to pick&choose from the list of services available on the platform so that they can easily get going without the hassles of provisioning services on their own. Let's look at an example of MySQL.

$ cf create-service p.mysql db-medium test-db$ cf bind-service spring-music db-medium$ cf env spring-musc
Getting env variables for app spring-music in org test-org / space dev as cloudnativejay@gmail.com...
OK
System-Provided:
{
"VCAP_SERVICES": {
"p-mysql": [
{
"credentials": {
"hostname": "10.0.4.35",
"port": 3306,
"name": "cf_2e23d10a_8738_8c3c_66cf_13e44422698c",
"username": "8McHri7aKbuTEGCR",
"password": "J2BNJYkeXAH9idkG",
"uri": "mysql://8McHri7aKbuTEGCR:J2BNJYkeXAH9idkG@10.0.4.35:3306/cf_2e23d10a_8738_8c3c_66cf_13e44422698c?reconnect=true",
"jdbcUrl": "jdbc:mysql://10.0.4.35:3306/cf_2e23d10a_8738_8c3c_66cf_13e44422698c?user=8McHri7aKbuTEGCR&password=J2BNJYkeXAH9idkG"
},
"syslog_drain_url": null,
"volume_mounts": [],
"label": "p-mysql",
"provider": null,
"plan": "100mb",
"name": "mysql",
"tags": [
"mysql",
"relational"
....

What has happened here is,

  1. cf create-service create a MySQL instance running at 10.0.4.35, with username and password autogenerated.

2. cf bind-service binds a MySQL Service instance to an application instance that exposes information in Environment Variable, VCAP_SERVICES.

3. "java-cfenv-boot" in the application dependency(build.gradle in Spring Music app) reads credentials from the JSON strings contained inside the VCAP_SERVICES environment variable and sets the standard Spring Boot configuration so that Auto-configuration can kick in.

On Azure Spring Apps, there could be many different options. I have written a few articles already before.

  1. Azure App Configuraiton
  2. Spring Cloud Config Server
  3. Azure Key Vault

Besides 3 options, I’d like to introduce a relatively new concept called Service Connector that could be a potential replacement for Service Instances. Service Connector will become a standard feature available for all the Azure Application platforms in the future. Let's explore the Service Connector with Spring Music app. On the Azure portal, there is a Service connector in the Settings. Developers can discover all the services available in their subscription like MySQL, Kafka, Redis, PostgreSQL, etc to start creating a connection.

Create a connection through browsing

Any of the services that an application consumes will require a basic URL to connect to and authentication information at a minimum. For example, MySQL requires three values: Jdbc URL, username, and password. By discovering the service on Service Connector, we don't have to worry about the URL as it's auto-populated. The rest of the values can be either read from Key Vault, or we put them right from the portal. You can choose to store your values in Key Vault for secure storage.

Creating connector with Username Password, store in Azure Key Vault

Once Service Connector is created, restart an app and go to the actuator endpoint.

Service Connector

Actuator propertySource clearly shows the role of the Service Connector. In short, it injects the property file into the container that is sourced to PropertySource. It cleverly uses the standard Spring Boot properties(spring.datasource.url, spring.datasource.username, spring.datasource.password) that will trigger the autoconfiguration. Look at the MySQL example at https://docs.microsoft.com/en-us/azure/service-connector/how-to-integrate-mysql#java---spring-boot-jdbc-secret--connection-string Or Redis Example at https://docs.microsoft.com/en-us/azure/service-connector/how-to-integrate-redis-cache#java---spring-boot-spring-boot-starter-data-redis-secret--connection-string

The one thing missing from ASA is that there is no way to share Service Connector between apps where you can do it with Service Instances on Cloud Foundry with cf share-service .

Spring Cloud Services(SCS)

This section is specifically for developers using Tanzu Application Service, which has a service called Spring Cloud Service for VMWare Tanzu. SCS is a marketplace service on TAS that developers can create a Spring Cloud Config Server and Spring Clod Eureka Discovery Server. Like other services, using SCS start with creating Service Instance.

$ cf create-service -c '{ "git": { "uri": "https://github.com/spring-cloud-samples/config-repo" }, "count": 3 }' p.config-server standard my-config-server
...
$ cf bind-service myapp my-config-server

Then from the application side, it requires the special dependency which is available on the Spring initializr.

Config Client (TAS), Service Registry (TAS)

Unlike TAS, Azure Spring Apps uses standard Spring Cloud dependencies. Also, Config Server and Eureka Server are pre-provisioned with the platform. It only requires standard OSS Spring Cloud dependencies as below.

Config Client, Eureka Discovery Client

Spring Cloud Config Server, just like SCS on Cloud Foundry, requires the backing repository. Developers can set it up on the Azure portal.

Repository configuration on Azure portal

If you're experiencing an issue that requires remote access to those services, check this link.

Checking Eureka Registry App Registration

APM Integration

APM Integration for popular services like New Relic, App Dynamics, Dynatrace, Datadog, Elastic, etc. on Cloud Foundry work by Service Instance and buildpack collaboration in tandem. Developers should create a User Provided Service type of Service Instance (cups) with an access key, etc., then bind it to their app so that buildpack detects the settings and injects the agent inside the droplet. Buildpack has a specific rule to detect APM integration by checking the name of the service. Let's look at the example of App Dynamics.

Detection Criterion used by Java Buildpack on Cloud Foundry
$ cf cups app-dynamics -p '{"account-access-key":"ACCESS-KEY", "account-name":"ACCOUNT", "application-name":"APP", "host-name":"HOST", "plan-description":"PLAN", "plan-name":"NAME", "port":"PORT", "ssl-enabled":TRUE|FALSE'$ cf service-bind spring-music app-dynamics
...

On Azure Spring Apps, Cloud Native Buildpack can do the same job, not by cups(of course) but by environment variables. Here is an example of Spring Apps with App Dynamics integration.

$ az spring app deploy \
--resource-group "<your-resource-group-name>" \
--service "<your-Azure-Spring-Apps-instance-name>" \
--name "<your-app-name>" \
--jar-path app.jar \
--jvm-options="-javaagent:/opt/agents/appdynamics/java/javaagent.jar" \
--env APPDYNAMICS_AGENT_APPLICATION_NAME=<your-app-name> \
APPDYNAMICS_AGENT_ACCOUNT_ACCESS_KEY=<your-agent-access-key> \
APPDYNAMICS_AGENT_ACCOUNT_NAME=<your-agent-account-name> \
APPDYNAMICS_JAVA_AGENT_REUSE_NODE_NAME=true \
APPDYNAMICS_JAVA_AGENT_REUSE_NODE_NAME_PREFIX=<your-agent-node-name> \
APPDYNAMICS_AGENT_TIER_NAME=<your-agent-tier-name> \
APPDYNAMICS_CONTROLLER_HOST_NAME=<your-AppDynamics-controller-host-name> \
APPDYNAMICS_CONTROLLER_SSL_ENABLED=true \
APPDYNAMICS_CONTROLLER_PORT=443

Wrapping Up

I’ve touched upon the most frequently used features of Cloud Foundry and its equivalent on Azure Spring Apps for Cloud Foundry developers. As you see in the article, simple Spring Boot apps will run just fine on Azure Spring Apps without any refactoring, but some apps especially with heavy use of java-cfenv-boot or unique Service Binding usage would require a little bit of refactoring. I hope this lays a nice foundation of understanding for Cloud Foundry developers for future migration.

If you see anything that I fail to cover in the article, let me know in the comments below, please. I will keep adding it based on the feedback.

If you like my article, please leave some claps here or maybe even start following me. You can hit me up on Linkedin. Thanks!

--

--

Jay Lee

Cloud Native Enthusiast. Java, Spring, Python, Golang, Kubernetes.