Moving to cloud-native
Cloud computing has brought many methodologies and techniques that have revolutionized both the business and technical worlds. Among the terms that came up was cloud-native. To meet and cover these expectations in the Java universe, Jakarta EE emerged. The purpose of this post is to talk about the concept of cloud-native and to run an application with this concept using the latest milestone version of Jakarta EE NoSQL.
What’s cloud-native?
Like any new concept, there are several concepts with the same name; if you read books or articles about cloud-native, you may not find consensus about it. For example:
Cloud-native is an approach to building and running applications that exploits the advantages of the cloud computing model. Pivotal
Cloud-native is a different way of thinking and reasoning about software systems. It embodies the following concepts: powered by disposable infrastructure, composed of bounded, scales globally, embraces disposable architecture. Architecting Cloud Native Applications: Design high-performing and cost-effective applications for the cloud
In general usage, “cloud-native” is an approach to building and running applications that exploits the advantages of the cloud-computing delivery model. “Cloud-native” is about how applications are created and deployed, not where. Infoworld
In a mutual consensus around the definitions from several articles, we can say that cloud-native is a term used to describe container-based environments. So cloud-native isn't related to specific programming languages or frameworks or even to a cloud provider company, but to containers.
What are cloud-native best practices?
When we start to learn a new concept, we usually run to read about best practices to avoid mistakes and any code smell. With Object-Oriented Programming (OOP), we have the design patterns from the gang of four, in Java we have Effective Java, and when talking about architecture, we have both Clean Code and Clean Architecture. So the question is: what are the best practices for cloud-native?
As far as we know, there aren't best practices related specifically to cloud-native. But since the cloud is close to Agile methodology, there are several practices we can leverage to have a healthy, pain-free application:
- Manifesto for Agile Software Development
- Continuous integration
- Continuous delivery
- Domain-Driven Design
The most well-known practices related to any application that includes cloud computing are inspired by Martin Fowler’s Patterns of Enterprise Application Architecture and Refactoring.
The Twelve-Factor App
- Codebase: One codebase tracked in revision control, many deploys
- Dependencies: Explicitly declare and isolate dependencies
- Config: Store config in the environment
- Backing services: Treat backing services as attached resources
- Build, release, run: Strictly separate build and run stages
- Processes: Execute the app as one or more stateless processes
- Port binding: Export services via port binding
- Concurrency: Scale out via the process model
- Disposability: Maximize robustness with fast startup and graceful shutdown
- Dev/prod parity: Keep development, staging, and production as similar as possible
- Logs: Treat logs as event streams
- Admin processes: Run admin/management tasks as one-off processes
In summary, there aren't specific best practices for cloud-native yet, but there are patterns from Agile, microservices, and the twelve-factor app that are useful to follow.
Back to the code
In the introduction we explained in detail what cloud-native means, now let's return to our application and convert it as a cloud-native application. In the first post, we explained the model, the entity, and how Jakarta NoSQL works. So we'll take it from here and use the easiest way to handle queries with NoSQL and MongoDB with a repository.
import jakarta.nosql.mapping.Column;
import jakarta.nosql.mapping.Entity;
import jakarta.nosql.mapping.Id;
import javax.json.bind.annotation.JsonbVisibility;
import java.io.Serializable;
import java.util.Objects;
import java.util.Set;
@Entity
@JsonbVisibility(FieldPropertyVisibilityStrategy.class)
public class Hero implements Serializable {
@Id
private String name;
@Column
private Integer age;
@Column
private Set<String> powers;
}
import jakarta.nosql.mapping.Page;
import jakarta.nosql.mapping.Pagination;
import jakarta.nosql.mapping.Repository;
import java.util.stream.Stream;
public interface HeroRepository extends Repository<Hero, String> {
Stream<Hero> findAll();
Page<Hero> findAll(Pagination pagination);
Stream<Hero> findByPowersIn(String powers);
Stream<Hero> findByAgeGreaterThan(Integer age);
Stream<Hero> findByAgeLessThan(Integer age);
}
To make these services available, we'll create a rest application with JAX-RS as a resource class.
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.util.List;
import java.util.function.Supplier;
import static java.util.stream.Collectors.toList;
@ApplicationScoped
@Path("heroes")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class HeroResource {
private static final Supplier<WebApplicationException> NOT_FOUND =
() -> new WebApplicationException(Response.Status.NOT_FOUND);
@Inject
private HeroRepository repository;
@GET
public List<Hero> findAll() {
return repository.findAll()
.collect(toList());
}
@GET
@Path("/{id}")
public Hero findById(@PathParam("id") String id) {
return repository.findById(id).orElseThrow(NOT_FOUND);
}
@GET
@Path("seniors/{age}")
public List<Hero> findByOlder(@PathParam("age") Integer age) {
return repository.findByAgeGreaterThan(age)
.collect(toList());
}
@GET
@Path("youngs/{age}")
public List<Hero> findByYounger(@PathParam("age") Integer age) {
return repository.findByAgeLessThan(age)
.collect(toList());
}
@POST
public void save(Hero hero) {
repository.save(hero);
}
@PUT
@Path("/{id}")
public void update(@PathParam("id") String id, Hero hero) {
repository.save(hero);
}
@Path("/{id}")
@DELETE
public void delete(@PathParam("id") String name) {
repository.deleteById(name);
}
}
The application is ready; the last step we'll create is the configuration class that allows the connection with MongoDB. This is simple, we'll use Eclipse MicroProfile Configuration that has tight integration capabilities with Eclipse JNoSQL, the reference implementation of Jakarta NoSQL. The Eclipse MicroProfile Config is a solution to externalize configuration from Java applications and makes the third factor easy to follow.
import jakarta.nosql.document.DocumentCollectionManager;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.inject.Disposes;
import javax.enterprise.inject.Produces;
import javax.inject.Inject;
@ApplicationScoped
class DocumentManagerProducer {
@Inject
@ConfigProperty(name = "document")
private DocumentCollectionManager manager;
@Produces
public DocumentCollectionManager getManager() {
return manager;
}
public void destroy(@Disposes DocumentCollectionManager manager) {
manager.close();
}
}
The current configuration of an application can be accessed via ConfigProvider#getConfig().
A Config consists of the information collected from the registered org.eclipse.microprofile.config.spi.ConfigSource s. These ConfigSource s get sorted according to their ordinal. That way we can overwrite the configuration with lower importance from outside.
By default there are 3 ConfigSources:
- System.getProperties() (ordinal=400)
- System.getenv() (ordinal=300)
- all META-INF/microprofile-config.properties files on the ClassPath. (default ordinal=100, separately configurable via a config_ordinal property inside each file)
Therefore, the default values can be specified in the above files packaged with the application and the value can be overwritten later for each deployment. A higher ordinal number takes precedence over a lower number.
That implies we can have the configuration for the local environment as a file, one to test also as a file, and finally we can override all this information when we move it to the cloud.
document=document
document.database=conferences
document.settings.jakarta.nosql.host=localhost:27017
document.provider=org.eclipse.jnosql.diana.mongodb.document.MongoDBDocumentConfiguration
We now have a local configuration, so let's move our application with Jakarta EE based on the cloud-native approach. To make it really easy, we'll use a Platform-as-a-Service (PaaS) because you can move your application container-based style in cloud through infrastructure as code (IaC).
Infrastructure as code, or programmable infrastructure, means writing code, which can be done using a high-level language or any descriptive language to manage configurations and automate the provisioning of the infrastructure in addition to deployments.
Platform.sh structure
The Java application is ready to go! The next step is to set the Platform.sh files required to manage and deploy the application. In our first Java post, we took a deep dive into each of these three files in detail:
- One Router (.platform/routes.yaml). Platform.sh allows you to define the routes.
- Zero or more service containers (.platform/services.yaml). Platform.sh allows you to completely define and configure the topology and services you want to use on your project.
- One or more application containers (.platform.app.yaml). You control your application and the way it will be built and deployed on Platform.sh via a single configuration file.
The file that will change on this post is the service file, allowing you to define a database, search engine, cache, and so on. For this project, we'll set MongoDB instead of MySQL.
mongodb:
type: mongodb:3.6
disk: 1024
To read the environment configuration, Platform.sh supports the configuration-reader that allows easy integration. Platform.sh also supports an array frameworks and languages, including Java. In this post, we'll override the MongoDB configuration with the Java properties that will add transparency to the application—thanks to the Eclipse MicroProfile Configuration. With these files ready and pushed to the master, Platform.sh will create a set of containers within a cluster.
# This file describes an application. You can have multiple applications
# in the same project.
#
# See https://docs.platform.sh/user_guide/reference/platform-app-yaml.html
# The name of this app. Must be unique within a project.
name: app
# The runtime the application uses.
type: "java:8"
disk: 800
# The hooks executed at various points in the lifecycle of the application.
hooks:
build: |
wget https://github.com/stedolan/jq/releases/download/jq-1.6/jq-linux64
mv jq-linux64 jq
chmod +x jq
mvn -U -DskipTests clean package payara-micro:bundle
# The relationships of the application with services or other applications.
#
# The left-hand side is the name of the relationship as it will be exposed
# to the application in the PLATFORM_RELATIONSHIPS variable. The right-hand
# side is in the form `<service name>:<endpoint name>`.
relationships:
mongodb: 'mongodb:mongodb'
# The configuration of app when it is exposed to the web.
web:
commands:
start: |
export MONGO_PORT=`echo $PLATFORM_RELATIONSHIPS|base64 -d|json_pp|./jq -r ".mongodb[0].port"`
export MONGO_HOST=`echo $PLATFORM_RELATIONSHIPS|base64 -d|json_pp|./jq -r ".mongodb[0].host"`
export MONGO_ADDRESS="${MONGO_HOST}:${MONGO_PORT}"
export MONGO_PASSWORD=`echo $PLATFORM_RELATIONSHIPS|base64 -d|json_pp|./jq -r ".mongodb[0].password"`
export MONGO_USER=`echo $PLATFORM_RELATIONSHIPS|base64 -d|json_pp|./jq -r ".mongodb[0].username"`
export MONGO_DATABASE=`echo $PLATFORM_RELATIONSHIPS|base64 -d|json_pp|./jq -r ".mongodb[0].path"`
java -jar -Xmx1024m -Ddocument.settings.jakarta.nosql.host=$MONGO_ADDRESS \
-Ddocument.database=$MONGO_DATABASE -Ddocument.settings.jakarta.nosql.user=$MONGO_USER \
-Ddocument.settings.jakarta.nosql.password=$MONGO_PASSWORD \
-Ddocument.settings.mongodb.authentication.source=$MONGO_DATABASE \
target/heroes-microbundle.jar --port $PORT
The application is now ready, so it’s time to move it to the cloud with Platform.sh using the following steps:
- Create a new free trial account.
- Sign up with a new username and password, or login using a current GitHub, Bitbucket, or Google account. If you use a third-party login, you’ll be able to set a password for your Platform.sh account later.
- Select the region of the world where your site should live.
- Select the blank template.
After this wizard, Platform.sh will provision the whole infrastructure to you, and it will offer your project a remote Git repository. The Platform.sh Git-driven infrastructure means it will automatically manage everything your application needs to push it to the master remote repository. After you set up your SSH keys, you only need to write your code—including a few YAML files that specify your desired infrastructure—then commit it to Git, and push.
git remote add platform <platform.sh@gitrepository>
git commit -m "Initial project"
git push -u platform master
In this post, we talked about the principles and best practices around cloud-native, which is still an area that needs improvement when we’re talking about a new software development technique. Cloud facilitates software development, and we can see an application running quite simply through Platform.sh and integrated with Jakarta EE. All to say, it’s a great time (happy new year!) to bring your project to a mature cloud PaaS like Platform.sh.