This monolithic UI can lead to unnecessary complexity and can often result in scaling difficulties, performance issues, and other problems as frontend developers try to keep pace with changes to backend microservices. To try and prevent a project ending with a monolithic UI, this blog post describes how to apply the twelve-factor app (link resides outside ibm.com) methodology to the creation of UI microservices.
A microservices architecture often starts with a focus on only the creation of the backend microservices. This approach can lead to a monolithic UI that combines and surfaces different functions and data from modular backend microservices. These large and complex UIs go against the fundamental concepts of microservice-based architecture, which is to enable multiple microservices to handle their own functions and tasks across both the backend and frontend. With our own application development, we work towards bringing modularity to both the backend and frontend of our applications. As we develop our UI microservices, we pay close attention to the twelve-factor app methodology for the Kubernetes model.
The twelve-factor app methodology provides a well-defined guideline for developing microservices. This methodology is a commonly used pattern to follow to run and scale microservices. This blog focuses on applying the 12 factors to UI microservices development, along with applying additional key factors that are specific to UI microservices and are supported by the Kubernetes model for container orchestration. The details for applying these factors are broken down into three overall categories for Kubernetes-based UI microservices:
“One codebase tracked in revision control, many deploys.”
Typically, an application is composed of multiple components, with each component supporting backend and UI functions. In a microservices architecture, each component — including any composite UI microservice — should be developed independently of other microservices by dedicated development teams. A composite UI microservice aggregates the UI from other microservices by following a pattern where the UI microservices are decoupled from the composite UI, while still providing a single-pane-of-glass experience. When you develop UI microservices, the code base should follow revision control and a single service with multiple deploys pattern.
Consider the following core principles when you are designing your UI microservices codebase:
The following diagram shows multiple separate UI microservices that plug into a main composite UI microservice at runtime to give a consistent experience:
Although each UI microservice is independent and can adopt its own choice of technology, designing microservices to use the same technology allows each microservice to share common components and drive consistency. For example, the composite UI and UI microservices in the preceding diagram can adopt different technologies, such as Node.js, React, and JavaScript. These UI microservices can be created using a source control repository, such as a Git repository. Then, specific versions of these UI containerized images can be stored in Docker Hub (link resides outside ibm.com).
With Docker Hub versions available, you can reference a specific image version in the container spec for pods and deployments. With this approach, you can have different versions of a microservice running in your development, staging, and production environments. Applications in these environments can then behave differently based on the configurations for each microservice version:
“Strictly separate build and run stages.”
Decoupled UI microservices provide a strict separation of build, release, and run phases. Each microservice team is responsible for completing tasks to commit code and build Docker images using the build pipeline. Node package manager can be used to install dependent packages for any Node-based UI microservice. The Docker image can also be published in the artifactory. You can then use the Helm Kubernetes Package Manager or Red Hat OpenShift Operators to package your application. These releases can be tagged and used in different development, staging, and production environments:
“Keep development, staging, and production as similar as possible.”
UI microservices can have dependencies on data from different backend microservices. These UI microservices should be designed to be deployed with the same architecture in any environment for consistency. Essentially, UI microservices should be able to handle various error conditions, such as backend API errors and application domain specific errors. Fault tolerance — such as when data or dependent services are unavailable — should be built into each UI microservice. This fault tolerance should include the composite UI, which should be tolerant towards any contributing UI microservice being unavailable.
Typically, UI microservices are developed and tested locally, which is not a production-ready approach. CI/CD processes need to run integration builds with key automated tests to catch integration issues as early as possible. For example, UI microservices can run Selenium-based functional tests with pull request builds and long-running Nightwatch-based tests running once a day to simulate a production-like data workload. The following screenshot shows a Selenium test output that is integrated with a Travis CI build:
“Explicitly declare and isolate dependencies.”
UI microservices should be stateless and clearly declare all dependencies. Isolate the header and any authentication or authorization functions that are required for UI microservices into separate services.
With Kubernetes, you can use liveliness and readiness probes to clearly declare and check for dependent services. The following diagram shows UI microservices that use readiness probes to check for required services, such as a header service, authorization service, and backing API services. Liveliness probes check whether the UI service is healthy. API services use readiness probes to check whether other data services or provider services are up and available. The composite UI checks whether UI services are discovered, and if any services are not discovered, the UI menu for those missing services does not display:
The following screenshot shows a liveliness and readiness probe YAML definition:
“Store config in the environment.”
UI microservices typically connect to backing API services. The configuration for connecting to these backing services should be stored in a ConfigMap or in Secrets to ensure that UI microservices are independent of the configurations. These configurations can be moved to different environments without requiring modifications to the source code. A simple, but very effective approach.
Factor VI: Process
“Execute the app as one or more stateless processes.”
UI microservices should be stateless by design. This statelessness enables scaling and failure recovery features to be easily implemented with containerized UI microservices that leverage Kubernetes container orchestration.
For example, if you had a UI microservice uiMicroservice1, you can update the microservice deployment within the uiMicroservice1 namespace to use three replicas through the following kubectl command:
kubectl patch deployment uiMicroservice1 -n uiMicroservice1 --type json -p='[{"op": "replace", "path": "/spec/replicas", "value":"3"}]
Then, if you run a kubectl get pods command, your output can include three pods similar to the following output:
“Treat backing services as attached resources.”
For example, a composite UI microservice should treat modular UI microservices as backing services. The supporting modular UI microservices should be accessed as services and specified in the configuration so that the the supporting modular microservice can be changed without affecting the composite UI and other modular UI microservices. The modular UI microservices can also have API as a backing service. Usually an API backing service collects data from different providers — such as data sources — and then normalizes and transforms the data into the format that the UI needs.
“Export services via port binding.”
Each UI microservice and all dependent backend services need to be exposed through a well-defined port. You can use Ingress to control external access and expose services externally.
For example, the following diagram shows UI microservices that use Ingress to control access and expose services externally. These UI microservices can access dependent API services using well-defined service ports. The composite UI microservice constructs the main navigation menu from the different endpoints to provide a consistent single-pane-of-glass experience to users.
When you are designing your port bindings, ensure that your routes do not conflict. As a tip, you can run different instances of your service in different Kubernetes namespaces:
“Scale out via the process model.”
As much as possible, UI microservices should remain stateless. This approach allows for horizontal and vertical scaling of the UI.
“Maximize robustness with fast startup and graceful shutdown.”
For UI microservices, the idea that processes should be disposable means that when an application stops abruptly, the user should not be affected. You can achieve this result by using Kubernetes-provided ReplicaSets. With ReplicaSets, you can control multiple sets of stateless UI microservices, and Kubernetes will maintain a level of availability for the microservices.
“Treat logs as event streams.“
UI microservices must report health and diagnostic information that provides insights to various events so that problems can be detected and diagnosed. This information helps to correlate events between independent microservices. Establish standard practices for your UI and other microservices to achieve a single logging format and to establish how to log health and diagnostic information for each service.
“Run admin/management tasks as one-off processes.“
Essentially, admin tasks should be isolated. This goal for UI microservices is no different than it is for any other microservice.
In addition to the preceding 12 factors, we pay close attention to the following additional factors when developing production-grade enterprise applications. Adhering to these factors can be beneficial to you in your application development.
“Apps should provide visibility about current health and metrics.“
Web interfaces need to be resilient and available 24/7 to meet business demand. When moving from monolithic UI to a modular microservices-based UI architecture, microservices grow in number and the communication between the microservices becomes more complex. Observability for microservices is critical for gaining visibility into communication failures and reacting to failures quickly.
As you design your UI microservices, use the following methods to help you make your microservices observable:
“Applications should provide guidance on expected resource constraints.“
Like any other microservice, UI microservices should provide guidance on expected resource constraints for CPU and memory usage to ensure Kubernetes reserves the required resources for the microservices. You can define request and limits for CPU and memory in the deployment config. For example, the following screenshot shows how to define requests and memory limits for a UI microservice uiMicroservice1 :
“Apps must upgrade data formats from previous generations.“
Incremental upgrades for UI microservices are frequently required to release features on shorter delivery cycles. Upgrades without service disruptions are important when upgrading any service. An important feature to understand and support for any dependent API service is backwards compatibility so that no breaking changes are introduced from upgrades. We use Operators to deploy our microservices in Kubernetes and leverage the Operator pattern to manage upgrades.
The following diagram shows how you can leverage the Operator pattern to independently manage UI microservice upgrades. In this diagram, the UI and API Operator and product images are pushed to a Red Hat Quay.io repository.
Application Operators are deployed in namespace1 and packaged as the Catalog Source. The Operator Source provides the endpoint to receive updates from the Quay.io registry. When the Catalog Source receives updates about the version v2 of the microservice, the Catalog Source updates the subscription based on the preference, which can be automatic or manual:
“Containers should be running with the least privilege.“
Incorrect or excessive permissions that are assigned to pods and containers pose a security threat and can lead to compromised pods. UI microservices need to access API services, Ingress services, and other essential services. When you design your microservices, consider the following areas when you are assigning privileges to pods and containers:
“Know what, when, who, and where for all critical operations.“
Well-designed UI microservices are stateless and typically call API backing services to get data. These microservices should have clear audit trails of who did what, which should be tracked through API services.
“Protect the app and resources from the outsiders.“
As a best practice, you should consider incorporating the following key security factors that UI microservices might need to provide:
We hope you have found this topic interesting. If you are in the middle of containerizing an application UI to deploy in Kubernetes, record the factors that you already applied and apply any factors that you are missing. Share your perspective with others.
Thanks for reading.
If you found this article interesting, take a look at this related article:
Thanks to Robert Wellon for reviewing this article.
Unlock the potential of DevOps to build, test and deploy secure cloud-native apps with continuous integration and delivery.
Learn how containers revolutionize the way businesses develop, deploy and manage applications. Discover why this technology is a game-changer for scalability, security and efficiency in today’s IT landscape.
Explore how Kubernetes enables businesses to handle large-scale applications, improve resource efficiency and achieve faster software delivery cycles. Learn how adopting Kubernetes can optimize your IT infrastructure and boost operational efficiency.
Discover how microservices evolved from traditional design patterns and learn about the essential principles driving today’s software development. Uncover how modern application architectures optimize performance, scalability and agility.