Chapter 1. Revisiting Enterprise Development
Enterprise development has always been one of the most exciting fields of software engineering, and the last decade has been a particularly fascinating period. The 2010s saw highly distributed microservices gradually replace classic three-tier architectures, with the almost limitless resources of cloud-based infrastructure pushing heavyweight application servers toward obsolescence. While developers are challenged with putting the pieces of the distributed world back together, plenty of voices question the necessity for this complex microservices world. The reality is that most applications are still well-crafted monolithic applications that follow a traditional software development process.
However, the way we deploy and operate software has changed equally fast. We have seen DevOps growing into GitOps, expanding developers’ responsibilities beyond the application code including the required infrastructure. Building on Markus’s book Modern Java EE Design Patterns (O’Reilly), this book puts more perspective on modernization than just modularization. We want to help you understand the various pieces that lead to a modern Kubernetes-native development platform and how to build and maintain applications on top of it.
This book aims to step back and evaluate the success factors and drivers for application modernization and cloud native architectures. We focus on modernizing Java-based Enterprise Applications, including a selection process for which applications are suitable for modernization and an overview of tools and methodologies that help you manage your modernization efforts. Instead of talking about patterns, this book provides a set of examples to help you apply everything you’ve learned.
That said, this book isn’t discussing monolithic versus distributed applications extensively. Rather, our goal is to help you understand how to seamlessly move your applications to the cloud.
You can use this book as a reference and read chapters in any order. We have organized the material, though, starting with higher-level concepts to implementation in iterations. First, it’s important to start by looking at the different definitions of clouds and how we build applications for them.
From Public to Private. Why Clouds?
The differences between public clouds, private clouds, hybrid clouds, and multiclouds were once easily defined by location and ownership. Today, these two are no longer the only relevant drivers for the classification of clouds. Let’s start with a more comprehensive definition of the different target environments and why they are used.
A public cloud environment is usually created from resources not owned by the end user that can be redistributed to other tenants. Private cloud environments solely dedicate their resources to the end user, usually within the user’s firewall, data center, or sometimes on premises. Multiple cloud environments with some degree of workload portability, orchestration, and management are called hybrid clouds. Decoupled, independent, and not connected clouds are commonly referred to as multiclouds. Hybrid and multicloud approaches are mutually exclusive; you can’t have both simultaneously because the clouds will either be interconnected (hybrid cloud) or not (multicloud).
Deploying applications to a cloud, regardless of the type of cloud, is becoming more common across enterprises as they seek to improve security and performance through an expanded portfolio of environments. But security and performance are only two of many reasons to move workloads into hybrid or multicloud environments. The primary motivation for many is the pay-for-what-you-use model. Instead of investing in costly on-premises hardware that is hard and expensive to scale out, clouds offer resources when you need them. You don’t have to invest in facilities, utilities, or building out your own data center. You do not even need dedicated IT teams to handle your cloud data center operations, as you can enjoy the expertise of your cloud provider’s staff.
For developers, the cloud is about self-service and flexibility. You don’t have to wait for environments to be promoted, and you can choose infrastructure components (e.g., databases, message brokers, etc.) as the need arises to free you from unnecessary wait times and ultimately speed up development cycles. Beyond these primary advantages, you can also find custom features for developers in some cloud environments. OpenShift, for example, has an integrated development console that provides developers with direct edit access to all details of their application topology. Cloud-based IDEs (e.g., Eclipse Che) provide browser-based access to development workspaces and eliminate local environment configuration for teams.
Additionally, cloud infrastructures encourage you to automate your deployment processes. Deployment automation enables you to deploy your software to testing and production environments with the push of a button—a mandatory requirement for Agile development and DevOps teams. You’ve seen a need for 100% automation already when you’ve read about microservices architectures. But automation goes well beyond the application parts. It extends to the infrastructure and downstream systems. Ansible, Helm, and Kubernetes Operators help you. We talk more about automation in Chapter 4, and you’ll use an Operator in Chapter 7.
What “Cloud Native” Means
You’ve probably heard of the cloud native approach for developing applications and services, and even more so since the Cloud Native Computing Foundation (CNCF) was founded in 2015 and released Kubernetes v1. Bill Wilder first used the term “cloud native” in his book, Cloud Architecture Patterns (O’Reilly). According to Wilder, a cloud native application is architected to take full advantage of cloud platforms by using cloud platform services and scaling automatically. Wilder wrote his book during a period of growing interest in developing and deploying cloud native applications. Developers had various public and private platforms to choose from, including Amazon AWS, Google Cloud, Microsoft Azure, and many smaller cloud providers. But hybrid-cloud deployments were also becoming more prevalent around then, which presented challenges.
The CNCF defines “cloud native” as:
cloud native technologies empower organizations to build and run scalable applications in modern, dynamic environments such as public, private, and hybrid clouds. Containers, service meshes, microservices, immutable infrastructure, and declarative APIs exemplify this approach.
These techniques enable loosely coupled systems that are resilient, manageable, and observable. Combined with robust automation, they allow engineers to make high-impact changes frequently and predictably with minimal toil.
CNCF Cloud Native Definition v1.0
Similar to cloud native technologies are the Twelve-Factor Apps. The Twelve-Factor Apps manifesto defines patterns for building applications that are delivered on the cloud. While these patterns overlap with Wilder’s cloud architecture patterns, the Twelve-Factor methodology can be applied to apps written in any programming language and use any combination of backing services (database, queue, memory cache, etc.).
Kubernetes-Native Development
For developers deploying applications to a hybrid cloud, shifting focus from cloud native to Kubernetes-native makes sense. One of the first mentions of “Kubernetes-native” is found as early as 2017. A blog post on Medium describes the differences between Kubernetes-native and cloud native as a set of technologies that are optimized for Kubernetes. The key takeaway is that Kubernetes-native is a specialization of cloud native and not separated from what cloud native defines. Whereas a cloud native application is intended for the cloud, a Kubernetes-native application is designed and built for Kubernetes.
In the early days of cloud native development, orchestration differences prevented applications from being genuinely cloud native. Kubernetes resolves the orchestration problem, but Kubernetes does not cover cloud provider services (for example, Roles and Permissions) or provide an event bus (for example, Kafka). The idea that Kubernetes-native is a specialization of cloud native means that there are many similarities between them. The main difference is cloud provider portability. Taking full advantage of the hybrid cloud and using multiple cloud providers requires that applications are deployable to any cloud provider. Without such a feature, you’re tied into a single cloud provider and reliant on them being up 100% of the time. To fully use the benefits of a hybrid cloud, applications have to be build in a Kubernetes-native way. Kubernetes-native is the solution to cloud portability concerns. We talk more about Kubernetes-native in Chapter 2.
Containers and Orchestration for Developers
One key ingredient for portability is the container. A container represents a fraction of the host system resources together with the application. The origins of containers go back to early Linux days with the introduction of chroots, and they became mainstream with Google’s process containers, which eventually became cgroups. Their use exploded in 2013 primarily because of Docker, which made them accessible for many developers. There is a difference between Docker the company, Docker containers, Docker images, and the Docker developer tooling we’re all used to. While everything started with Docker containers, Kubernetes prefers to run containers through any container runtime (e.g. containerd or CRI-O) that supports its Container Runtime Interface (CRI). What many people refer to as Docker images are actually images packaged in the Open Container Initiative (OCI) format.
Container-Native Runtime
Containers offer a lighter-weight version of the Linux operating system’s userland stripped down to the bare essentials. However, it’s still an operating system, and the quality of a container matters just as much as the host operating system. It takes a lot of engineering, security analysis, and resources to support container images. It requires testing not just the base images but also their behavior on a given container host. Relying on certified and OCI-compliant base images removes hurdles when moving applications across platforms. Ideally, these base images already come with the necessary language runtimes you need. For Java-based applications, the Red Hat Universal Base Image is a good starting point. We’ll learn more about containers and how developers use them in Chapter 4.
Kubernetes Flavors
We’ve talked about Kubernetes as a general concept so far. And we continue to use the word Kubernetes to talk about the technology that powers container orchestration. The name Kubernetes (or sometimes just K8s) refers to the open source project that is widely understood to be the standards body for the core functionality of container orchestration. We use the term “plain” Kubernetes throughout the book if we refer to standard functionality inside Kubernetes. The Kubernetes community created different distributions and even flavors of Kubernetes. The CNCF runs the Certified Kubernetes Conformance Program, which lists over 138 products from 108 vendors at the time of writing. The list contains complete distributions (e.g., MicroK8s, OpenShift, Rancher), hosted offerings (e.g., Google Kubernetes Engine, Amazon Elastic Kubernetes Service, Azure AKS Engine), and installers (e.g., minikube, VanillaStack). They all share the common core but add additional functionality or integrations on top as the vendors see a need or opportunity. We don’t make any suggestions about which Kubernetes flavor to use in this book. You will have to decide on your own which direction you want to take your production workloads. To help you run the examples in this book locally, we use minikube and do not require you to have a full-blown installation somewhere in a cloud.
Managing Development Complexity
One of the most critical areas of Kubernetes-native development is the management of your development environment. The number of tasks that need to be executed for a successful deployment or staging into multiple environments has grown exponentially. One reason is the growing number of individual application parts or microservices. Another reason is the application-specific configuration of necessary infrastructure. Figure 1-1 gives a brief overview of an example development environment with tools necessary for a fully automated development. We will talk about a fraction of them in this book to give you an easy start in your new environment. The core development tasks haven’t changed. You will still write an application or service with a suitable framework, like Quarkus, as we do in this book. This part of the developer workflow is commonly referred to as “inner loop” development.
We will spend most of our time in this book walking through changes and opportunities in the “outer loop.” The outer loop takes your built and tested application and puts it into production through various mechanisms. It is essential to understand that we are expressing some very strong opinions in this book. They reflect what we have learned about making Java developers productive, fast, and maybe even happy by using the tools and techniques we are recommending. As indicated in Figure 1-1, you have one or two choices to make in some places. We chose the more traditional way for Java developers in this book. We use Maven instead of Gradle for the application build and podman over Docker to build the container images. We also use the OpenJDK and not GraalVM and stick with JUnit instead of Testcontainers in the examples.
But the cloud native ecosystem, as mapped out by the CNCF landscape, has even more tools for you to choose from. Think of this book as a trail map for the Enterprise Java developer.
Besides the technology choices, you’ll also have to decide how you want to use this new ecosystem. With the variety of tools available comes another dimension that lets you choose your level of engagement with Kubernetes. We differentiate between opinionated and flexible as outlined in Figure 1-2. As a developer obsessed with details, you might want to learn all of the examples from the trenches and use plain Kubernetes while crafting your YAML files.
Note
Originally, YAML was said to mean Yet Another Markup Language. This name was intended as a tongue-in-cheek reference to its purpose as a markup language. But it was later repurposed as YAML Ain’t Markup Language, a recursive acronym, to distinguish its purpose as data-oriented.
You may decide to focus exclusively on source code and don’t want distraction from implementing business logic. This can be achieved with developer tools provided by some distributions. Depending on what’s most important to you in your development process, there are various options. You can use the main Kubernetes command-line interface (CLI) kubctl
instead of a product-specific one like OpenShift’s CLI oc
. If you want to be closer to a complete product, we suggest you try CodeReady Containers. It is an OpenShift cluster on your laptop with an easy getting started experience. But, the choice is yours.
Another great tool we would recommend is odo, which is a general-purpose developer CLI for Kubernetes based projects. Existing tools such as kubectl
and oc
are more operations-focused and require a deep understanding of underlying concepts. Odo abstracts away complex Kubernetes concepts for the developer. Two example choices from the outer development loop are Continous Integration (CI) solutions. We use Tekton in this book, and you can use it in Chapter 6. It is also possible to use Jenkins on Kubernetes with the Jenkins Operator or even Jenkins X. Whatever choice you make, you will be the master of your Kubernetes-native journey after all.
DevOps and Agility
When modernizing your Enterprise Java application, the next critical change is in the creation of cross-functional teams that share responsibilities from idea to operation. While some say that DevOps is solely focused on the operational aspects and paired with self-service for developers, we firmly believe that DevOps is a team culture focused on long-lasting impact. The word “DevOps” is a mashup of “development’ and “operations,” but it represents a set of ideas and practices much more significant than simply the sum of both terms. DevOps includes security, collaborative ways of working, data analytics, and many other things. DevOps describes approaches to speeding up the processes by which a new business requirement goes from code in development to deployment in a production environment. These approaches require that development teams and operations teams communicate frequently and work with empathy for their teammates. Scalability and flexible provisioning are also necessary. Developers, usually coding in a familiar development environment, work closely with IT operations to speed software builds, tests, and releases without sacrificing reliability. All this together results in more frequent code changes and more dynamic infrastructure usage. Changing a traditional IT organization from a traditional approach to a DevOps methodology is often described as transformation and is way beyond the scope of this book. Nevertheless, it is an essential ingredient, and you will see this transformation is beautifully described as “teaching elephants to dance” in books, articles, and presentations.
Summary
In this chapter, you learned some basic definitions and heard about the most important technologies and concepts we are going to use throughout this book. The next chapter takes you into the source code of your first application modernization.
Get Modernizing Enterprise Java now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.