DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports Events Over 2 million developers have joined DZone. Join Today! Thanks for visiting DZone today,
Edit Profile Manage Email Subscriptions Moderation Admin Console How to Post to DZone Article Submission Guidelines
View Profile
Sign Out
Refcards
Trend Reports
Events
Zones
Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

Microservices

A microservices architecture is a development method for designing applications as modular services that seamlessly adapt to a highly scalable and dynamic environment. Microservices help solve complex issues such as speed and scalability, while also supporting continuous testing and delivery. This Zone will take you through breaking down the monolith step by step and designing a microservices architecture from scratch. Stay up to date on the industry's changes with topics such as container deployment, architectural design patterns, event-driven architecture, service meshes, and more.

icon
Latest Refcards and Trend Reports
Trend Report
Microservices and Containerization
Microservices and Containerization
Refcard #379
Getting Started With Serverless Application Architecture
Getting Started With Serverless Application Architecture
Refcard #346
Microservices and Workflow Engines
Microservices and Workflow Engines

DZone's Featured Microservices Resources

Microservices Discovery With Eureka

Microservices Discovery With Eureka

By Jennifer Reif CORE
Gaining complexity in a microservices system certainly isn't for the faint of heart (though neither is complexity in monoliths!). When there are many services that need to communicate with one another, we might need to coordinate multiple services communicating with multiple other services. We also might code for varying environments such as local, development server, or the cloud. How do services know where to find one another? How can we avoid problems when a service is unavailable? How do we handle requests when we scale up or down certain parts of our system? This is where something like Spring Cloud Netflix Eureka comes into play. Eureka is a service discovery project that helps services interact with one another without hardwiring in instance-specific or environment-dependent details. Architecture We have carefully built up a system of microservices from generic application chatter to a system of services communicating among one another. In the last article, we migrated most of our standalone services into Docker Compose so that it could orchestrate startup and shutdown as a unit. In this article, we will add a service discovery component, so that services can find and talk to one another without hard-coding host and port information into applications or environments. Docker Compose manages most of the services (in dark gray area), with each containerized service encompassed in a light gray box. Neo4j is the only component managed externally with Neo4j's database-as-a-service (AuraDB). Interactions between services are shown using arrows, and the types of data objects passed to numbered services (1-4) are depicted next to each. Spring Cloud Netflix Eureka Spring Cloud Netflix originally contained a few open-sourced projects from Netflix, including Eureka, Zuul, Hystrix, and Ribbon. Since then, most of those have been migrated into other Spring projects, except for Eureka. Eureka handles service registry and discovery. A Eureka server is a central place for services to register themselves. Eureka clients register with the server and are able to find and communicate with other services on the registry without referencing hostname and port information within the service itself. Config + Eureka Architecture Decision I had to make a decision on architecture when using Spring Cloud Config and Eureka together in a microservices system. There are a couple of options: 1. Config-first approach. Applications (services1-4) will reach out to config server first before gathering up other properties. In this approach, the config server does not register with Eureka. 2. Discovery-first approach. Applications will register with Eureka before connecting to config and gathering properties. In this approach, config server becomes a Eureka client and registers with it. There is an excellent blog post that provides a clear explanation of each, along with pros and cons. I'd highly encourage checking that out! I opted for the config-first approach because there is already a bit of delay starting up applications in Docker Compose (see blog post detailing this). Going with discovery-first would mean an extra step in the chain before applications could connect to config and contact databases. Since I didn't want to slow this step down any further, I decided not to register the config server app with Eureka, leaving it separate. Without further ado, let's start coding! Applications: Eureka Server We will use the Spring Initializr at start.spring.io to set up the outline for our Eureka server application. On the form, we choose Maven for the Project, then leave Language and Spring Boot version fields defaulted. Under the Project Metadata section, I updated the group name for my personal projects, but you are welcome to leave it defaulted. I named the artifact eureka-server, though naming is up to you, as long as we map it properly where needed. All other fields in this section can remain as they are. Under the Dependencies section, we need only Eureka Server. Finally, we can click the Generate button at the bottom to download the project. The project will download as a zip, so we can unzip it and move it to our project folder with the other services. Open the project in your favorite IDE and let's get coding! The pom.xml contains the dependencies and software versions we set up on the Spring Initializr, so we can move to the application.properties file in the src/main/resources folder. Properties files server.port=8761 eureka.client.register-with-eureka=false eureka.client.fetch-registry=false We need to specify a port number for this application to use so that its traffic doesn't conflict with our other services. The default port for Spring Cloud Eureka server is 8761, so we will use that. Next, we don't need to register the server itself with Eureka (useful in systems with multiple Eureka servers), so we will set the eureka.client.register-with-eureka value to false. The last property is set to false because we also don't need this server to pull the registry from other sources (like other Eureka servers). A StackOverflow question and answer addresses these settings well. In the EurekaServerApplication class, we only need to add the annotation @EnableEurekaServer to set this up as a Eureka server. Let's test this locally by starting the application in our IDE and navigating a web browser window to localhost:8761. This should show us a page like the one below, which gives details about the server and a section for Instances currently registered with Eureka. Since we haven't connected any other services with Eureka, we don't have any services registered with the server. That's it for the server, so let's start retrofitting our other services as Eureka clients. Applications: Service1 We don't have many changes to add for Spring Cloud Eureka. Starting in the pom.xml, we need to add a dependency. XML <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> This dependency enables the application as a Eureka client. Most recommendations would also have us adding an annotation like @EnableEurekaClient (Eureka-specific) or @EnableDiscoveryClient (project-agnostic) to the main application class. However, that is not a necessary requirement, as it is defaulted to enabling this functionality when you add the dependency to the pom.xml. To run the service locally, we will also need to add a property to the `application.properties` file. Properties files eureka.client.serviceUrl.defaultZone=http://localhost:8761/eureka This tells the application where to look for the Eureka server. We will move this property to the config server file for this application, so we can comment this one out when we test everything together. However, for testing a siloed application, you will need it enabled here. Let's start on changes to service2, which interacts with service1. Applications: Service2 Just like with service1, we need to add the Eureka client dependency to service2's pom.xml to enable service discovery. XML <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-config</artifactId> </dependency> We also want to have this application use Spring Cloud Config for referencing the Eureka server, so we can retrofit that by adding the dependency. We will walk through the config file changes in a bit. Again, if we test locally, we would also need to add the following property to the application.properties file. Properties files eureka.client.serviceUrl.defaultZone=http://localhost:8761/eureka Since we will test everything together, it is commented out in the application for now. Instead, we will add a properties file for Spring Cloud Config to host, similar to our other services (next section). Next, we need to make some adjustments to the main application class to utilize Eureka over previously-defined hostname and port locations. Java public class Service2Application { public static void main(String[] args) { SpringApplication.run(Service2Application.class, args); } @Bean @LoadBalanced WebClient.Builder createLoadBalancedBuilder() { return WebClient.builder(); } @Bean WebClient client(WebClient.Builder builder) { return builder.baseUrl("http://mongo-client").build(); } } Eureka lets calling applications reference an application name, and it will map the hostname/port details behind-the-scenes, no matter where the application is running. This is where we see the mongo-client referenced in the second @Bean definition (11th line of above code). We also need to create a load-balanced bean (only required when using Eureka). Step-by-step, I created a WebClient.Builder bean, load balanced it with the @LoadBalanced annotation, then used that to create the actual WebClient bean that gets injected for use in method calls (in the BookController class). Applications: Service3 and Service4 Next, we need to add our other services to Eureka using the steps below. 1. Add the dependency to each pom.xml file. 2. For local testing, add the commented out property in the application.properties file. Now let's add the Eureka property to the Spring Cloud Config files for our applications! Spring Cloud Config For each config file the server hosts, we will need to add the following: YAML eureka: client: serviceUrl: defaultZone: http://goodreads-eureka:8761/eureka This tells the application where to look so it can register with Eureka. Full sample code for each config file is located in the related Github repository folder. We also need to create a whole new config file for service2 to use the config server. YAML spring: application: name: goodreads-client eureka: client: serviceUrl: defaultZone: http://goodreads-eureka:8761/eureka A sample is provided on the Github repository, but this file is created in a local repository initialized with git, and then referenced in the config server properties file for that project to serve up. More information on that is in a previous blog post. Let's make a few changes to the docker-compose.yml! docker-compose.yml We need to remove the dynamic environment property for service2 and to add the Eureka server project for Docker Compose to manage. YAML goodreads-svc2: #other properties... environment: - SPRING_APPLICATION_NAME=goodreads-client - SPRING_CONFIG_IMPORT=configserver:http://goodreads-config:8888 - SPRING_PROFILES_ACTIVE=docker We added environment variables for application name, config server location, and spring profiles like we see in our other services. Next, we need to add our Eureka server application to the compose file. YAML goodreads-eureka: container_name: goodreads-eureka image: jmreif/goodreads-eureka ports: - "8761:8761" environment: - EUREKA_CLIENT_REGISTER-WITH-EUREKA=false - EUREKA_CLIENT_FETCH-REGISTRY=false volumes: - $HOME/Projects/docker/goodreads/config-server/logs:/logs networks: - goodreads For our last step, we need to build all of the updated applications and create the Docker images. To do that we can execute the following commands from the project folder: Shell cd service1 mvn clean package -DskipTests=true cd ../service2 mvn clean package -DskipTests=true cd ../service3 mvn clean package -DskipTests=true cd ../service4 mvn clean package -DskipTests=true cd ../eureka-server mvn clean package Note: the Docker Compose file is using my pre-built images with Apple silicon architecture. If your machine has a different chip, you will need to do one of the following: 1) utilize the build option in the docker-compose.yml file (comment out image option), 2) create your own Docker images and publish to DockerHub (plus modify the docker-compose.yml file image options). We can run our system with the same command we have been using. Shell docker-compose up -d Note: If you are building local images with the `build` field in docker-compose.yml, then use the command `docker-compose up -d --build`. This will build the Docker containers each time on startup from the directories. Next, we can test all of our endpoints. Goodreads-config (mongo): command line with curl localhost:8888/mongo-client/docker. Goodreads-eureka: web browser with localhost:8761 and note the applications (might take a few minutes for everything to register). Goodreads-svc1: command line with curl localhost:8081/db, curl localhost:8081/db/books, and curl localhost:8081/db/book/623a1d969ff4341c13cbcc6b. Goodreads-svc2: command line with curl localhost:8080/goodreads and curl localhost:8080/goodreads/books. Goodreads-svc3: curl localhost:8082/db, curl localhost:8082/db/authors, and curl localhost:8082/db/author/623a48c1b6575ea3e899b164. Goodreads-config (neo4j): command line with curl localhost:8888/neo4j-client/docker. Neo4j database: ensure AuraDB instance is running (free instances are automatically paused after 3 days). Goodreads-svc4: curl localhost:8083/neo, curl localhost:8083/neo/reviews, and curl localhost:8083/neo/reviews/178186 or web browser with only URL. Bring everything back down again with the below command. Shell docker-compose down Wrapping Up! In this iteration of the project, we integrated service discovery through the Spring Cloud Netflix Eureka project. We created a Eureka server project, and then retrofitted our other services as Eureka clients with an added dependency. Finally, we integrated the new Eureka server project to Docker Compose and updated some of the options for the other services. We tested all of our changes by spinning up the entire microservices system and checking each of our endpoints. Keep following along in this journey to find out what comes next (or review previous iterations to see what we have accomplished). Happy coding! Resources Github: microservices-level10 repository Blog post: Baeldung's guide to Spring Cloud Netflix Eureka Blog post: Config First vs. Discovery First Documentation: Spring Cloud Netflix Interview questions: What is Spring Cloud Netflix? More
Secrets Management

Secrets Management

By Niranjan Limbachiya CORE
Today's digital businesses are expected to innovate, execute, and release products at a lightning-fast pace. The widespread adoption of automation tools, when coupled with DevOps and DevSecOps tools, is instrumental to these businesses achieving increased developer velocity and faster feedback loops. This eventually helps in shortening release cycles and improving the product quality in an iterative manner. Though the shift to microservices and containerized applications and the adoption of open source are helping developers ship faster, they also pose challenges related to compliance and security. As per the Hidden In Plain Sight report from 1Password, DevOps and IT teams in enterprises continually face challenges posed by leakage of secrets, insecure sharing of secrets, and manual secrets management, amongst others. There are significant complexities involved in managing secrets like API keys, passwords, encryption keys, and so on for large-scale projects. Let’s take a deep dive into the integral aspects of secrets management in this article. What Is Secrets Management? In simple terms, secrets are non-human privileged credentials that give developers the provision to access resources in applications, containers, and so on. Akin to passwords management, secrets management is a practice whereby secrets (e.g., access tokens, passwords, API keys, and so on) are stored in a secure environment with tighter access controls. Managing secrets can become mayhem as the complexity and scale of an application grows over time. Additionally, there could be situations where secrets are being shared across different blocks across the technology stack. This could pose severe security threats, as it opens up the back doors for malicious actors to access your application. Secrets management ensures that sensitive information is never hard-coded and available only in an encrypted format. Secure access to sensitive data in conjunction with RBAC (role-based access controls) is the secret sauce of secrets management. Challenges of Secret Management There might be numerous cases where developers could have accidentally used hard-coded plain-text format credentials in their code or configuration files. The repercussions to the business could be huge if the respective files housing the secrets are pushed to the designated public repository on GitHub (or any other popular code hosting platforms). The benefits offered by multi-cloud infrastructures, containerized applications, IoT/IIoT, CI/CD, and similar advancements can be leveraged to the maximum extent by also focusing on efficient management of secrets. Educating development and DevOps teams about application security is the foremost step to build a security-first culture within the team. Here are the major challenges DevOps and DevSecOps teams face when managing secrets: Secrets Sprawl This scenario normally arises when the team’s (and/or organization’s) secrets are distributed across the organization. Digital-first organizations are increasingly using containers and cloud-based tools to increase developer velocity, save costs, and expedite releases. The same principle also applies for the development and testing of IoT-based applications. Depending on the scale and complexity of the applications, there is a high probability that the secrets are spread across: Containerized microservices-based applications (e.g., Kubernetes, OpenShift, Nomad) Automated E2E testing/tracing platforms (e.g., Prometheus, Graphite) Internally developed tools/processes Application servers and databases DevOps toolchain The items in the above list vary depending on the scale, size, and complexity of the application. Providing RBAC, using strong rotating passwords, and avoiding password sharing are some of the simple practices that must be followed at every level within the team/organization. Proliferation of Cloud Developer and Testing Tools Irrespective of the size and scale of the project, development teams look to maximize the usage of cloud development tools like GCP (Google Cloud Platform), Microsoft Azure, AWS (Amazon Web Services), Kubernetes, and more. Cloud tools definitely expedite processes related to development and testing, but they must be used while keeping security practices at the forefront. Any compromise of keys used for accessing the respective cloud platform (e.g., AWS keys) could lead to financial losses. AWS Credentials publicly exposed in a repository With so much at stake, DevOps and development teams must ensure that any sort of keys are not available in human-readable format in public domains (e.g., GitHub repositories). Organizations focusing on community-led growth (CLG) for evangelizing their product or developer tool need to ensure that their users do not leave any keys out in the open! If keys are left publicly accessible, hackers could exploit your platform for malicious reasons. Manual processes for managing secrets, data security when using third-party resources (e.g., APIs), and end-to-end visibility from the security lens are other challenges that organizations face with secrets management. Best Practices of Secret Management There is no one-size-fits-all approach in securely managing secrets, since a lot depends on the infrastructure, product requirements, and other such varying factors. Keeping variables aside, here are some of the best practices when it comes to efficient and scalable management of secrets: Use RBAC (Role-Based Access Control) Every project and organization has sensitive data and resources that must be accessible only by trusted users and applications. Any new user in the system must be assigned default privilege (i.e., minimum access control). Elevated privileges must be available only to a few members in the project or organization. The admin (or super-admin) must have the rights to add or revoke privileges of other members on a need basis. Escalation of privileges must also be done on a need basis, and only for a limited time. Proper notes must be added when giving/revoking privileges so that all the relevant project stakeholders have complete visibility. Use Secure Vaults In simple terms, a vault is a tool that is primarily used for securing any sensitive information (e.g., passwords, API keys, certificates, and so on). Local storage of secrets in a human readable form is one of the worst ways to manage secrets. This is where secure vaults can be extremely useful, as they provide a unified interface to any secret, along with providing a detailed audit log. Secure vaults can also be used for instrumenting role-based access control (RBAC) by specifying access privileges (authorization). Hashicorp Vault Helm chart and Vault for Docker are two of the popular vault managers that can be used for running vault services, accessing and storing secrets, and more. Since most applications leverage the potential of the cloud, it is important to focus on data security when it is in transit or at rest. This is where EaaS (Encryption as a Service) can be used for offloading encryption needs of applications to the vault before the data is stored at rest. Rotate Keys Regularly It is a good security practice to reset keys after a few weeks or months. One practice is to manually regenerate the keys, since there is a probability that applications using the secrets might be leaving behind traces in the log files or centralized logging systems. Attackers can get back-door access to the logs and use it to exfiltrate secrets. Additionally, co-workers might unintentionally leak secrets outside the organization. To avoid such situations, it is recommended to enable rotation of secrets in the respective secrets management tool. For instance, Secrets Manager rotation in AWS Secrets Manager uses an AWS Lambda function to update the secret and the database. Above all, teams should have practices in place to detect unauthorized access to the system. This will help in taking appropriate actions before significant damage can be done to the business. Why Implement Secrets Management in a DevSecOps Pipeline? Accelerated release cycles and faster developer feedback can only be achieved if the code is subjected to automated tests in a CI/CD pipeline. The tests being run in the CI pipeline might require access to critical protected resources like databases, HTTP servers, and so on. Even running unit tests inside Docker containers is also a common practice, but developers and QAs need to ensure that secrets are not stored inside a Dockerfile. Secret management tools can be used in conjunction with popular CI/CD tools (e.g., Jenkins) whereby keys and other secrets are managed in a centralized location. Secrets are also stored with encryption and tokenization. More
How To Approach Legacy System Modernization
How To Approach Legacy System Modernization
By Hiren Dhaduk
Visual Network Mapping Your K8s Clusters To Assess Performance
Visual Network Mapping Your K8s Clusters To Assess Performance
By Anton Lawrence CORE
How Can MACH Architecture Boost Digital Transformation?
How Can MACH Architecture Boost Digital Transformation?
By Nisarg Mehta
Distributed Caching on Cloud
Distributed Caching on Cloud

Distributed caching is an important aspect of cloud-based applications, be it for on-premises, public, or hybrid cloud environments. It facilitates incremental scaling, allowing the cache to grow and incorporate the data growth. In this blog, we will explore distributed caching on the cloud and why it is useful for environments with high data volume and load. This blog will cover the following: Traditional Caching Challenges What is Distributed Caching Benefits of Distributed Caching on cloud Recommended Distributed Caching Database Tools Ways to Deploy Distributed Caching on Hybrid Cloud Traditional Caching Challenges Traditional caching servers are usually deployed with limited storage and CPU speed. Often these caching infrastructures reside in data centers that are on-premises. I am referring to a non-distributed caching server. Traditional distributed caching comes with numerous challenges: Hard-to-scale cache storage and CPU speed on non-cloud node servers. High operational cost to manage infrastructure and unutilized hardware resources. Inability to scale and manage traditional distributed caching (since it is non-containerized). Possibility of servers crashing if client load is higher than actual. Chances of stale data during programmatic sync-up with multiple data center servers. Slow data synchronization between servers and various data centers. What Is Distributed Caching? Caching is a technique to store the state of data outside of the main storage and store it in high-speed memory to improve performance. In a microservices environment, all apps are deployed with their multiple instances across various servers/containers on the hybrid cloud. A single caching source is needed in a multicluster Kubernetes environment on the cloud to persist data centrally and replicate it on its own caching cluster. It will serve as a single point of storage to cache data in a distributed environment. Benefits of Distributed Caching on Cloud Periodic caching of frequently used read REST APIs' response ensures faster API read performance. Reduced database network calls by accessing cached data directly from distributed caching databases. Resilience and fault tolerance by maintaining multiple copies of data at various caching databases in a cluster. High availability by auto-scaling the cache databases based on load or client requests. Storage of secret session tokens like JSON Web Token (ID/JWT) for authentication and authorization purposes for microservices apps containers. Faster read and write access in-memory if it's used as a dedicated database solution for high-load mission-critical applications. Avoid unnecessary roundtrip data calls to persistent databases. Auto-scalable cloud infrastructure deployment. Containerization of distributed caching libraries/solutions. Consistent read data from any synchronized connected caching data centers. Minimal to no outage, high availability of caching data. Faster data synchronization between caching data servers. Recommended Distributed Caching Database Tools The following are popular industry-recognized caching servers: Redis Memcached GemFire Hazelcast databases Redis It's one of the most popular distributed caching services. It supports different data structures. It's an open-source, in-memory data store used by millions of developers as a database, cache, streaming engine, and message broker. It also has an enterprise version. It can be deployed in containers on private, public, and hybrid clouds, providing consistent and faster data synchronization between different data centers. Memcached It is an open-source, high-performance, distributed memory object caching system. It is generic in nature but intended for use in speeding up dynamic web applications by alleviating database load. Memcached is an in-memory key-value store for small chunks of arbitrary data (strings, objects) from the results of database calls, API calls, or page rendering. Memcached is simple yet powerful. Its simple design promotes easy, quick deployment and development. It solves many data-caching problems, and the API is available in various commonly used languages. GemFire It provides distributed in-memory data-grid cache powered by Apache Geode open source. It scales data services on demand to support high performance. It's a key-value store that performs read and write operations at fast speeds. In addition, it offers highly available parallel message queues, continuous availability, and an event-driven architecture to scale dynamically with no downtime. It provides multisite replication. As data size requirements increase to support high-performance, real-time apps, they can scale linearly with ease. Applications get low-latency responses to data-access requests and always return fresh data. It maintains transaction integrity across distributed nodes and supports high-concurrency, low-latency data operations of the application. It also provides node failover and cross-data center or multi-datacenter replication to ensure applications are resilient, whether on-premises or in the cloud. Hazelcast Hazelcast is a distributed computation and storage platform for consistent low-latency querying, aggregation, and stateful computation against event streams and traditional data sources. It allows you to quickly build resource-efficient, real-time applications. You can deploy it at any scale, from small-edge devices to a large cluster of cloud instances. A cluster of Hazelcast nodes share both the data storage and computational load, which can dynamically scale up and down. When you add new nodes to the cluster, the data is automatically rebalanced across the cluster. The computational tasks (jobs) that are currently in a running state snapshot their state and scale with a processing guarantee. Ways to Deploy Distributed Caching on Hybrid Cloud These are recommended ways to deploy, and setup distributed caching, be it on the public cloud or hybrid cloud: Open source distributed caching on traditional VM instances. Open source distributed caching on Kubernetes container. I would recommend deploying on a Kubernetes container for high availability, resiliency, scalability, and faster performance. Enterprise commercial off-the-shelf distributed caching deployment on VM and container. I would recommend the enterprise version because it will provide additional features and support. The public cloud offers managed services of distributed caching for open-source and enterprise tools like Redis, Hazelcast and Memcached, etc. Caching servers can be deployed on multiple sources like on-premises and public cloud together, public servers, or only one public server in different availability zones. Conclusion Distributed caching is now a de-facto requirement for distributed microservices applications in a distributed deployment environment on a hybrid cloud. It addresses concerns in important use cases like maintaining user sessions when cookies are disabled on the web browser, improving API query read performance, avoiding operational costs and database hits for the same type of requests, managing secret tokens for authentication and authorization, etc. Distributed cache syncs data on hybrid clouds automatically without any manual operation and always gives the latest data. I would recommend industry-standard distributed caching solutions like Redis, Hazelcast, and Memcached. We need to choose a better distributed caching technology in the cloud based on use cases.

By Vishal Padghan
Improve Microservices Security by Applying Zero-Trust Principles
Improve Microservices Security by Applying Zero-Trust Principles

This is an article from DZone's 2022 Enterprise Application Security Trend Report.For more: Read the Report According to a 2020 Gartner report, it is estimated that by 2023, 75 percent of cybersecurity incidents will result from inadequate management of identities and excessive privileges. To a large extent, this is attributable to the increased number of identities used by modern cloud infrastructures. Applications run as microservices in fully virtualized environments that consist of dynamically orchestrated clusters of multiple containers in the cloud. The security requirements in such environments are significantly different compared to monolithic applications running on-premises. First, the concept of the perimeter does not exist in the cloud. Second, organizations are now handling thousands of dynamically created workloads and identities. Applying traditional IAM tools to manage the dynamic nature of these identities is not adequate. Using static, long-lived, and often excessive access permissions enables attackers to perform lateral movement. To address these issues, a security model is needed that better satisfies today's application security and identity requirements. Zero-trust security is a proactive security model that uses continuous verification and adaptive security controls to protect endpoints and access to applications as well as the data that flows between them. Zero trust replaces the outdated assumption that everything running inside an organization's network can be implicitly trusted. This security model has proven to minimize the attack surface, offer threat protection against internal and external attackers, reduce the lateral movement of attackers, increase operational efficiency, and help support continuous compliance with regulations such as PCI-DSS and the White House's 2021 Cybersecurity Executive Order. Since its inception, zero trust has evolved and expanded, touching almost every corner of the enterprise. This article will provide an overview of how the zero-trust principles can be applied in a microservices environment and what security controls should be implemented on the back end. Zero-Trust Principles Zero trust is primarily based on the concepts of "never trust, always verify" and "assume everything is hostile by default." It is driven by three core principles: assume breach, verify explicitly, and the principle of least privilege. Assume Breach Always assume that cyber attacks will happen, the security controls have been compromised, and the network has been infiltrated. This requires using redundant and layered security controls, constant monitoring, and collection of telemetry to detect anomalies and respond in real-time. Verify Explicitly No network traffic, component, action, or user is inherently trusted within a zero-trust security model, regardless of location, source, or identity. Trust only to the extent that you verify the identity, authenticity, permissions, data classification, etc. Principle of Least Privilege Always grant the least number of privileges. Only give access for the time that it is needed and remove access when it is not needed anymore. Least privilege access is essential to reduce the attack surface, limit the "blast radius," and minimize an attacker's opportunity to move laterally within an environment in case of compromise. Zero-Trust Security in a Microservices Environment When a microservice is compromised, it may maliciously influence other services. By applying the principles of zero trust to a microservices environment, the trust between services, components, and networks is eliminated or minimized. Identity and Access Management Identity and access management is the backbone of zero trust, which requires strong authentication and authorization of end-user identities, services, functions, workloads, and devices. To enable authentication and authorization, we must first ensure that each workload is automatically assigned a cryptographically secure identity that is validated on every request. Importantly, ensure that there is an automated mechanism to reliably distribute, revoke in case of compromise, and frequently rotate the services' certificates and secrets. Use a cloud-neutral identity for workloads, such as SPIFFE for authentication and OPA for unified authorization across the stack. Secure Service-To-Service Communications In zero trust, it is fundamental to treat the network as adversarial. Thus, all communication between services, APIs, and storage layers must be encrypted. The standard way of protecting data in transit is to use HTTPS and strict mTLS everywhere. Similarly, a strong authentication mechanism should be enforced across all microservices. It must be understood that not every service that can be authenticated should be authorized. Authorization must be done based on the authentication context and on access control policies, and it should be performed at the edge of each microservice — not at the network edge. To achieve this, use a service mesh, like Istio or Linkerd, for: Automatic certificate management Traffic interception Secure service-to-service communication without application code changes Micro-segmentation (via authorization policies) This reduces the blast radius of an attack and prevents attackers from pivoting from one compromised service into other parts of the infrastructure. In a container orchestration environment, such as Kubernetes, define network policies for egress and ingress isolation at a granular level. Enforce zero trust for all traffic (east-west and north-south) by specifying network policies and service-to-service level RBAC policies that limit access per cluster and per source, following the need-to-know principle. Secure Access to Resources External entities must not access the microservices environment directly. Instead, use an API gateway as a single entry point to the microservices deployment. To pass the user context or the identity of the caller, implement a pattern, such as the phantom token pattern (API Security in Action, part 11.6.1) or the passport pattern. Validate the external access token and user context at the edge and generate a new short-lived token that represents the external entity identity and is cryptographically signed by the trusted issuer and propagated to back-end microservices. Ensure that the new token's scope of access is as limited as the scope of the identity of the external entity. Most importantly, assume that access tokens can be stolen and create access tokens with a short lifespan on a resource-by-resource basis. Use a service mesh to verify the validity of the access tokens at the microservice edge. In all cases, access to resources should be granted using fine-grained role-based access controls with the least privileges. Figure 1: Data in-transit, data at-rest, and data in-use encryption Data Security It is essential to ensure that all data is classified according to their secrecy and confidentiality. Create a data registry to know which microservice handles what data. Then, implement multiple layers of data encryption, depending on the data classification. Do not trust only the encryption of external components (including databases and messaging systems like Kafka). Use application-level encryption (ALE) to transfer personally identifiable information (PII) and highly confidential data between microservices. To mitigate the risk of unauthorized data modification, perform data integrity checksums throughout the data lifecycle. Infrastructure Security Adopting an immutable infrastructure has become standard. Use Infrastructure as Code to provision components upfront and never change them after deployment. Do not trust the storage mediums (persistent or temporary) and do not store any sensitive data or secrets in an unencrypted form. All secrets, certificates, and API keys should be securely stored in access-controlled centralized key vaults. Zero trust always assumes that the network is compromised. To contain a possible compromise and prevent lateral spreading through the rest of the network, implement network micro-segmentation, create software-defined perimeters in each segment, and place microservices in each segment according to their functionality, business domain, and data classification. Communication between segments should be well-defined and controlled through API gateways. Consider adopting a cell-based architecture for inter-segment communication. Container and Cluster Security Zero trust requires the explicit verification of container images, containers, and cluster nodes. Thus, use container images that are signed only from trusted issuers and registries. Allow images to be used only if they are scanned in the DevSecOps pipeline and have no vulnerabilities. To reduce the risk of privilege escalation, run the Docker daemon and all containers without root privileges. One standard way is to run Docker in rootless mode. Logically isolate high-risk applications and workloads in the same cluster for the least number of privileges. Runtime Security Consider running security-sensitive microservices on confidential virtual machines in hardware-based trusted execution environments with encrypted memory. To reduce the risk of rogue or compromised nodes in the cluster, verify the integrity of nodes, VMs, and containers by running them on instances enabled with Secure Boot and Virtual Trusted Platform Module. Also, by running containers in read-only mode, filesystem integrity is achieved and attackers are prevented from making modifications. Finally, we can reduce our trust for the runtime by adopting a RASP solution that inspects all code executed by the runtime and dynamically stops the execution of malicious code. Figure 2: Zero-trust runtime via confidential computing and RASP Image adapted from "Application enclave support with Intel SGX based confidential computing nodes on AKS," Microsoft Azure Documentation Conclusion Implementing a zero-trust architecture is a critical defense-in-depth strategy and has become a mandatory security model in modern IT infrastructures. It is important to understand that implementing a zero-trust architecture does not mean zero security incidents. The goal is to continually layer security controls to increase the cost of attacks. As we introduce more friction into the cyber-attack kill chain, the attacker's value proposition will be reduced, and potential attacks will be disrupted. The key to a successful implementation of a zero-trust architecture is to follow the guidance of whitepapers such as NIST's "Planning for a Zero Trust Architecture" and the U.S. Office of Management and Budget's "Moving the U.S. Government Towards Zero Trust Cybersecurity Principles." In this article, we provided an overview of how to apply the core principles of the zero-trust model in a microservices environment, and we examined the critical areas and the zero-trust security goals of microservices that need to be achieved. The highly distributed and heterogeneous nature of a microservice deployment and its complex communication patterns has increased the number of different components and the volume of data that is exposed on the network. This provides a broader attack surface compared to a traditional deployment of a monolithic application. Because the security of a system is as good as its weakest link, applying the zero-trust core principles to proactively secure all layers and components of a microservices deployment is fundamental for a modern, reliable, and mature cybersecurity strategy. With a proper zero-trust strategy for microservices, the risk of compromised clusters, lateral movement, and data breaches in most cases can be eliminated. Zero trust is a necessary evolution to security; however, its implementation should not be a destination. It is a continuous journey and an organization-wide commitment. Since its inception, zero trust has become a widely deployed security model and a business-critical cybersecurity priority. Microsoft's 2021 Zero Trust Adoption Report confirms that point on page 11, indicating that 76 percent of organizations have started adopting a zero-trust strategy. The industry is rapidly adopting zero trust across the whole infrastructure and not just on end-user access. This is an article from DZone's 2022 Enterprise Application Security Trend Report.For more: Read the Report

By Apostolos Giannakidis
Spring Boot Docker Best Practices
Spring Boot Docker Best Practices

In this blog, you will learn some Docker best practices mainly focussed on Spring Boot applications. You will learn these practices by applying them to a sample application. Enjoy! 1. Introduction This blog continues where the previous blog about Docker Best Practices left off. However, this blog can be read independently from the previous one. The goal is to provide some best practices that can be applied to Dockerized Spring Boot applications. The Dockerfile that will be used as a starting point is the following: Dockerfile FROM eclipse-temurin:17.0.5_8-jre-alpine@sha256:02c04793fa49ad5cd193c961403223755f9209a67894622e05438598b32f210e WORKDIR /opt/app RUN addgroup --system javauser && adduser -S -s /usr/sbin/nologin -G javauser javauser ARG JAR_FILE COPY target/${JAR_FILE} app.jar RUN chown -R javauser:javauser . USER javauser ENTRYPOINT ["java", "-jar", "app.jar"] This Dockerfile is doing the following: FROM: Take eclipse-temurin:17 Java Docker image as base image; WORKDIR: Set /opt/app as the working directory; RUN: Create a system group and system user; ARG: provide an argument JAR_FILE so that you do not have to hard code the jar file name into the Dockerfile; COPY: Copy the jar file into the Docker image; RUN: Change the owner of the WORKDIR to the previously created system user; USER: Ensure that the previously created system user is used; ENTRYPOINT: Start the Spring Boot application. In the next sections, you will change this Dockerfile to adhere to best practices. The resulting Dockerfile of each paragraph is available in the git repository in the directory Dockerfiles. At the end of each paragraph, the name of the corresponding final Dockerfile will be mentioned where applicable. The code being used in this blog is available on GitHub. 2. Prerequisites The following prerequisites apply to this blog: Basic Linux knowledge Basic Java and Spring Boot knowledge Basic Docker knowledge 3. Sample Application A sample application is needed in order to demonstrate the best practices. Therefore, a basic Spring Boot application is created containing the Spring Web and Spring Actuator dependencies. The application can be run by invoking the following command from within the root of the repository: Shell $ mvn spring-boot:run Spring Actuator will provide a health endpoint for your application. By default, it will always return the UP status. Shell $ curl http://localhost:8080/actuator/health {"status":"UP"} In order to alter the health status of the application, a custom health indicator is added. Every 5 invocations, the health of the application will be set to DOWN. Java @Component public class DownHealthIndicator implements HealthIndicator { private int counter; @Override public Health health() { counter++; Health.Builder status = Health.up(); if (counter == 5) { status = Health.down(); counter = 0; } return status.build(); } } For building the Docker image, a fork of the dockerfile-maven-plugin of Spotify will be used. The following snippet is therefore added to the pom file. XML <plugin> <groupId>com.xenoamess.docker</groupId> <artifactId>dockerfile-maven-plugin</artifactId> <version>1.4.25</version> <configuration> <repository>mydeveloperplanet/dockerbestpractices</repository> <tag>${project.version}</tag> <buildArgs> <JAR_FILE>${project.build.finalName}.jar</JAR_FILE> </buildArgs> </configuration> </plugin> The advantage of using this plugin is that you can easily reuse the configuration. Creating the Docker image can be done by a single Maven command. Building the jar file is done by invoking the following command: Shell $ mvn clean verify Building the Docker image can be done by invoking the following command: Shell $ mvn dockerfile:build Run the Docker image: Shell $ docker run --name dockerbestpractices mydeveloperplanet/dockerbestpractices:0.0.1-SNAPSHOT Find the IP-address of the running container: Shell $ docker inspect dockerbestpractices | grep IPAddress "SecondaryIPAddresses": null, "IPAddress": "172.17.0.3", "IPAddress": "172.17.0.3" In the above example, the IP-address is 172.17.0.3. The application also contains a HelloController which just responds with a hello message. The Hello endpoint can be invoked as follows: Shell $ curl http://172.17.0.3:8080/hello Hello Docker! Everything is now explained to get started! 4. Best Practices 4.1 Healthcheck A healthcheck can be added to your Dockerfile in order to expose the health of your container. Based on this status, the container can be restarted. This can be done by means of the HEALTHCHECK command. Add the following healthcheck: Dockerfile HEALTHCHECK --interval=30s --timeout=3s --retries=1 CMD wget -qO- http://localhost:8080/actuator/health/ | grep UP || exit 1 This healthcheck is doing the following: interval: Every 30 seconds the healthcheck is executed. For production use, it is better to choose something like five minutes. In order to do some tests, a smaller value is easier. This way you do not have to wait for five minutes each time. timeout: A timeout of three seconds for executing the health check. retries: This indicates the number of consecutive checks which have to be executed before the health status changes. This defaults to three which is a good number for in-production. For testing purposes, you set it to one, meaning that after one unsuccessful check, the health status changes to unhealthy. command: The Spring Actuator endpoint will be used as a healthcheck. The response is retrieved and piped to grep in order to verify whether the health status is UP. It is advised not to use curl for this purpose because not every image has curl available. You will need to install curl in addition to the image and this enlarges the image with several MBs. Build and run the container. Take a closer look at the status of the container. In the first 30 seconds, the health status indicates starting because the first health check will be done after the interval setting. Shell $ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES ddffb5a9cbf0 mydeveloperplanet/dockerbestpractices:0.0.1-SNAPSHOT "java -jar /opt/app/…" 8 seconds ago Up 6 seconds (health: starting) dockerbestpractices After 30 seconds, the health status indicates healthy. Shell $ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES ddffb5a9cbf0 mydeveloperplanet/dockerbestpractices:0.0.1-SNAPSHOT "java -jar /opt/app/…" 33 seconds ago Up 32 seconds (healthy) dockerbestpractices After 2-5 minutes, the health status indicates unhealthy because of the custom health indicator you added to the sample application. Shell $ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES ddffb5a9cbf0 mydeveloperplanet/dockerbestpractices:0.0.1-SNAPSHOT "java -jar /opt/app/…" 2 minutes ago Up 2 minutes (unhealthy) dockerbestpractices Again, 30 seconds after the unhealthy status, the status reports healthy. Did you notice that the container did not restart due to the unhealthy status? That is because the Docker engine does not do anything based on this status. A container orchestrator like Kubernetes will do a restart. Is it not possible to restart the container when running with the Docker engine? Yes, it can: you can use the autoheal Docker image for this purpose. Let’s start the autoheal container. Shell docker run -d \ --name autoheal \ --restart=always \ -e AUTOHEAL_CONTAINER_LABEL=all \ -v /var/run/docker.sock:/var/run/docker.sock \ willfarrell/autoheal Verify whether it is running. Shell $ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES ddffb5a9cbf0 mydeveloperplanet/dockerbestpractices:0.0.1-SNAPSHOT "java -jar /opt/app/…" 10 minutes ago Up 10 minutes (healthy) dockerbestpractices d40243eb242a willfarrell/autoheal "/docker-entrypoint …" 5 weeks ago Up 9 seconds (healthy) autoheal Wait until the health is unhealthy again or just invoke the health actuator endpoint in order to speed it up. When the status reports unhealthy, the container is restarted. You can verify this in the STATUS column where you can see the uptime of the container. Shell $ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES ddffb5a9cbf0 mydeveloperplanet/dockerbestpractices:0.0.1-SNAPSHOT "java -jar /opt/app/…" 12 minutes ago Up 6 seconds (health: starting) dockerbestpractices You have to decide for yourself whether you want this or whether you want to monitor the health status yourself by means of a monitoring tool. The autoheal image provides you the means to automatically restart your Docker container(s) without manual intervention. The resulting Dockerfile is available in the git repository with the name 6-Dockerfile-healthcheck. 4.2 Docker Compose Docker Compose gives you the opportunity to start multiple containers at once with a single command. Besides that, it also enables you to document your services, even when you only have one service to manage. Docker Compose used to be installed separately from Docker, but nowadays it is part of Docker itself. You need to write a compose.yml file that contains this configuration. Let’s see what this looks like for the two containers you used during the healthcheck. YAML services: dockerbestpractices: image: mydeveloperplanet/dockerbestpractices:0.0.1-SNAPSHOT autoheal: image: willfarrell/autoheal:1.2.0 restart: always environment: AUTOHEAL_CONTAINER_LABEL: all volumes: - type: bind source: /var/run/docker.sock target: /var/run/docker.sock Two services (read: containers) are configured. One for the dockerbestpractices image and one for the autoheal image. The autoheal image will restart after a reboot, has an environment variable defined, and has a volume mounted. Execute the following command from the directory where the compose.yml file can be found: Shell $ docker compose up In the logging, you will see that both containers are started. Open another terminal window and navigate to the directory where the compose.yml can be found. A lot of commands can be used in combination with Docker Compose. E.g. show the status of the running containers. Shell $ docker compose ps NAME COMMAND SERVICE STATUS PORTS mydockerbestpracticesplanet-autoheal-1 "/docker-entrypoint …" autoheal running (healthy) mydockerbestpracticesplanet-dockerbestpractices-1 "java -jar /opt/app/…" dockerbestpractices running (healthy) Or stop the containers: Shell $ docker compose stop [+] Running 2/2 ⠿ Container mydockerbestpracticesplanet-autoheal-1 Stopped 4.3s ⠿ Container mydockerbestpracticesplanet-dockerbestpractices-1 Stopped 0.3s Or easily remove the containers: Shell $ docker compose rm ? Going to remove mydockerbestpracticesplanet-dockerbestpractices-1, mydockerbestpracticesplanet-autoheal-1 Yes [+] Running 2/0 ⠿ Container mydockerbestpracticesplanet-autoheal-1 Removed 0.0s ⠿ Container mydockerbestpracticesplanet-dockerbestpractices-1 Removed As you can see, Docker Compose provides quite some advantages and you should definitely consider using it. 4.3 Multi-Stage Builds Sometimes it can be handy to build your application inside a Docker container. The advantage is that you do not need to install a complete development environment onto your system and that you can interchange the development environment more easily. However, there is a problem with building the application inside your container. Especially when you want to use the same container for running your application. The sources and the complete development environment will be available in your production container and this is not a good idea from a security perspective. You could write separate Dockerfiles to circumvent this issue: one for the build and one for running the application. But this is quite cumbersome. The solution is to use multi-stage builds. With multi-stage builds, you can separate the building stage from the running stage. The Dockerfile looks as follows: Dockerfile FROM maven:3.8.6-eclipse-temurin-17-alpine@sha256:e88c1a981319789d0c00cd508af67a9c46524f177ecc66ca37c107d4c371d23b AS builder WORKDIR /build COPY . . RUN mvn clean package -DskipTests FROM eclipse-temurin:17.0.5_8-jre-alpine@sha256:02c04793fa49ad5cd193c961403223755f9209a67894622e05438598b32f210e WORKDIR /opt/app RUN addgroup --system javauser && adduser -S -s /usr/sbin/nologin -G javauser javauser COPY --from=builder /build/target/mydockerbestpracticesplanet-0.0.1-SNAPSHOT.jar app.jar RUN chown -R javauser:javauser . USER javauser HEALTHCHECK --interval=30s --timeout=3s --retries=1 CMD wget -qO- http://localhost:8080/actuator/health/ | grep UP || exit 1 ENTRYPOINT ["java", "-jar", "app.jar"] As you can see, this Dockerfile contains two FROM statements. The first one is used for building the application: FROM: A Docker image containing Maven and Java 17, this is needed for building the application; WORKDIR: Set the working directory; COPY: copy the current directory to the working directory into the container; RUN: The command in order to build the jar file. Something else is also added to the FROM statement. At the end, AS builder is added. This way, this container is labeled and can be used for building the image for running the application. The second part is identical to the Dockerfile you used to have before, except for two lines. The following lines are removed: Dockerfile ARG JAR_FILE COPY target/${JAR_FILE} app.jar These lines ensured that the jar file from our local build was copied into the image. These are replaced with the following line: Dockerfile COPY --from=builder /build/target/mydockerbestpracticesplanet-0.0.1-SNAPSHOT.jar app.jar With this line, you indicate that you want to copy a file from the builder container into the new image. When you build this Dockerfile, you will notice that the build container executes the build and finally, the image for running the application is created. During building the image, you will also notice that all Maven dependencies are downloaded. The resulting Dockerfile is available in the git repository with the name 7-Dockerfile-multi-stage-build. 4.4 Spring Boot Docker Layers A Docker image consists of layers. If you are not familiar with Docker layers, you can check out a previous post. Every command in a Dockerfile will result in a new layer. When you initially pull a Docker image, all layers will be retrieved and stored. If you update your Docker image and you only change for example the jar file, the other layers will not be retrieved anew. This way, your Docker images are stored more efficiently. However, when you are using Spring Boot, a fat jar is created. Meaning that when you only change some of your code, a new fat jar is created with unchanged dependencies. So each time you create a new Docker image, megabytes are added in a new layer without any necessity. For this purpose, Spring Boot Docker layers can be used. A detailed explanation can be found here. In short, Spring Boot can split the fat jar into several directories: /dependencies /spring-boot-loader /snapshot-dependencies /application The application code will reside in the directory application, whereas for example, the dependencies will reside in directory dependencies. In order to achieve this, you will use a multi-stage build. The first part will copy the jar file into a JDK Docker image and will extract the fat jar. Dockerfile FROM eclipse-temurin:17.0.4.1_1-jre-alpine@sha256:e1506ba20f0cb2af6f23e24c7f8855b417f0b085708acd9b85344a884ba77767 AS builder WORKDIR application ARG JAR_FILE COPY target/${JAR_FILE} app.jar RUN java -Djarmode=layertools -jar app.jar extract The second part will copy the split directories into a new image. The COPY commands replace the jar file. Shell FROM eclipse-temurin:17.0.4.1_1-jre-alpine@sha256:e1506ba20f0cb2af6f23e24c7f8855b417f0b085708acd9b85344a884ba77767 WORKDIR /opt/app RUN addgroup --system javauser && adduser -S -s /usr/sbin/nologin -G javauser javauser COPY --from=builder application/dependencies/ ./ COPY --from=builder application/spring-boot-loader/ ./ COPY --from=builder application/snapshot-dependencies/ ./ COPY --from=builder application/application/ ./ RUN chown -R javauser:javauser . USER javauser HEALTHCHECK --interval=30s --timeout=3s --retries=1 CMD wget -qO- http://localhost:8080/actuator/health/ | grep UP || exit 1 ENTRYPOINT ["java", "org.springframework.boot.loader.JarLauncher"] Build and run the container. You will not notice any difference when running the container. The main advantage is the way the Docker image is stored. The resulting Dockerfile is available in the git repository with the name 8-Dockerfile-spring-boot-docker-layers. 5. Conclusion In this blog, some best practices are covered when creating Dockerfiles for Spring Boot applications. Learn to apply these practices and you will end up with much better Docker images.

By Gunter Rotsaert CORE
Kubernetes-Native Inner Loop Development With Quarkus
Kubernetes-Native Inner Loop Development With Quarkus

Microservices today are often deployed on a platform such as Kubernetes, which orchestrates the deployment and management of containerized applications. Microservices, however, don't exist in a vacuum. They typically communicate with other services, such as databases, message brokers, or other microservices. Therefore, an application usually consists of multiple services that form a complete solution. But, as a developer, how do you develop and test an individual microservice that is part of a larger system? This article examines some common inner-loop development cycle challenges and shows how Quarkus and other technologies help solve some of these challenges. What Is the Inner Loop? Almost all software development is iterative. The inner loop contains everything that happens on a developer's machine before committing code into version control. The inner loop is where a developer writes code, builds and tests it, and perhaps runs the code locally. In today's world, the inner loop could also include multiple commits to a Git pull request, where a developer may commit multiple times against a specific feature until that feature is deemed complete. Note: The word local is also up for debate in industry today as more and more remote development environments, such as Red Hat OpenShift Dev Spaces, Gitpod, and GitHub Codespaces are available. This article does not differentiate between a developer machine and any of these kinds of environments. They are all viewed as local in this article. Inner loop shifts to outer loop when code reaches a point in source control where it needs to be built, tested, scanned, and ultimately deployed by automated continuous integration and deployment (CI/CD) processes. Figure 1 illustrates a simple inner loop and outer loop. Figure 1: The inner loop takes place on a developer's local machine, whereas the outer loop takes place within CI/CD processes. Challenges of Inner Loop Development Developing a single microservice in isolation is challenging enough without worrying about additional downstream services. How do you run a microservice in isolation on your local machine if it depends on other services for it to function properly? Using various mocking techniques, you can to some extent get around the absence of required services when writing and running tests. Mocking techniques generally work great for testing. You can also use in-memory replacements for required services, such as an H2 database instead of a separate database instance. Beyond that, if you want or need to run the application locally, you need a better solution. Of course, you could try to reproduce your application's entire environment on your development machine. But even if you could, would you really want to? Do you think a developer at Twitter or Netflix could reproduce their environment on their development machine? Figure 2 shows the complexity of their architectures. Lyft also tried this approach and found it wasn't feasible or scalable. Figure 2 (courtesy of https://future.com/the-case-for-developer-experience): Major services such as Netflix and Twitter can easily have more than 500 microservices. Container-based Inner Loop Solutions Using containers can help speed up and improve the inner loop development lifecycle. Containers can help isolate and provide a local instance of a dependent service. We'll look at a few popular tools and technologies for the inner loop. Docker Compose One common pattern is to use Docker Compose to run some of your microservice's dependent services (databases, message brokers, etc.) locally while you run, debug, and test your microservice. With Docker Compose, you define a set of containerized services that provide the capabilities required by your microservice. You can easily start, stop, and view logs from these containerized services. However, there are a few downsides to using Docker Compose. First, you must maintain your Docker Compose configuration independently of your application's code. You must remember to make changes to the Docker Compose configuration as your application evolves, sometimes duplicating configuration between your application and your Docker Compose configuration. Second, you are locked into using the Docker binary. For Windows and macOS users, Docker Desktop is no longer free for many non-individual users. You are also prevented from using other container runtimes, such as Podman. Podman does support Docker Compose, but it doesn't support everything you can do with Docker Compose, especially on non-Linux machines. Testcontainers Testcontainers is an excellent library for creating and managing container instances for various services when applications run tests. It provides lightweight, throwaway instances of common databases, message brokers, or anything else that can run in a container. But Testcontainers is only a library. That means an application must incorporate it and do something with it to realize its benefits. Generally speaking, applications that use Testcontainers do so when executing unit or integration tests, but not in production. A developer generally won't include the library in the application's dependencies because Testcontainers doesn't belong as a dependency when the application is deployed into a real environment with real services. Quarkus Quarkus is a Kubernetes-native Java application framework focusing on more than just feature sets. In addition to enabling fast startup times and low memory footprints compared to traditional Java applications, Quarkus ensures that every feature works well, with little to no configuration, in a highly intuitive way. The framework aims to make it trivial to develop simple things and easy to develop more complex ones. Beyond simply working well, Quarkus aims to bring Developer Joy, specifically targeting the inner loop development lifecycle. Dev Mode The first part of the Quarkus Developer Joy story, live coding via Quarkus dev mode, improves and expedites the inner loop development process. When Quarkus dev mode starts, Quarkus automatically reflects code changes within the running application. Therefore, Quarkus combines the Write Code, Build, and Deploy/Run steps of Figure 1's inner loop into a single step. Simply write code, interact with your application, and see your changes running with little to no delay. Dev Services A second part of the Quarkus Developer Joy story, Quarkus Dev Services, automatically provisions and configures supporting services, such as databases, message brokers, and more. When you run Quarkus in dev mode or execute tests, Quarkus examines all the extensions present. Quarkus then automatically starts any unconfigured and relevant service and configures the application to use that service. Quarkus Dev Services uses Testcontainers, which we've already discussed, but in a manner completely transparent to the developer. The developer does not need to add the Testcontainers libraries, perform any integration or configuration, or write any code. Furthermore, Dev Services does not affect the application when it is deployed into a real environment with real services. Additionally, if you have multiple Quarkus applications on your local machine and run them in dev mode, by default, Dev Services attempts to share the services between the applications. Sharing services is beneficial if you work on more than one application that uses the same service, such as a message broker. Let's use the Quarkus Superheroes sample application as an example. The application consists of several microservices that together form an extensive system. Some microservices communicate synchronously via REST. Others are event-driven, producing and consuming events to and from Apache Kafka. Some microservices are reactive, whereas others are traditional. All the microservices produce metrics consumed by Prometheus and export tracing information to OpenTelemetry. The source code for the application is on GitHub under an Apache 2.0 license. The system's architecture is shown in Figure 3. Figure 3: The Quarkus Superheroes application has many microservices and additional dependent services. In the Quarkus Superheroes sample application, you could start both the rest-fights and event-statistics services locally in dev mode. The rest-fights dev mode starts a MongoDB container instance, an Apicurio Registry container instance, and an Apache Kafka container instance. The event-statistics service also requires an Apicurio Registry instance and an Apache Kafka instance, so the instance started by the rest-fights dev mode will be discovered and used by the event-statistics service. Continuous Testing A third part of the Quarkus Developer Joy story, continuous testing, provides instant feedback on code changes by immediately executing affected tests in the background. Quarkus detects which tests cover which code and reruns only the tests relevant to that code as you change it. Quarkus continuous testing combines testing with dev mode and Dev Services into a powerful inner loop productivity feature, shrinking all of Figure 1's inner loop lifecycle steps into a single step. Other Inner Loop Solutions The solutions we've outlined thus far are extremely helpful with local inner loop development, especially if your microservice requires only a small set of other services, such as a database, or a database and message broker. But when there are lots of dependent services, trying to replicate them all on a local machine probably won't work well, if at all. So what do you do? How do you get the speed and agility of inner loop development for an application when it depends on other services that you either can't or don't want to run locally? One solution could be to manage an environment of shared services. Each developer would then configure those services in their local setup, careful not to commit the configuration into source control. Another solution could be to use Kubernetes, giving each developer a namespace where they can deploy what they need. The developer could then deploy the services and configure their local application to use them. Both of these solutions could work, but in reality, they usually end up with a problem: The microservice the developer is working on is somewhere in the graph of services of an overall system. How does a developer trigger the microservice they care about to get called as part of a larger request or flow? Wouldn't a better solution be to run the application locally, but make the larger system think the application is actually deployed somewhere? This kind of remote + local development model is becoming known as remocal. It is an extremely powerful way to get immediate feedback during your inner loop development cycle while ensuring your application behaves properly in an environment that is close to or matches production. Quarkus Remote Development Another part of the Quarkus Developer Joy story, remote development, enables a developer to deploy an application into a remote environment and run Quarkus dev mode in that environment while doing live coding locally. Quarkus immediately synchronizes code changes to the remotely deployed instance. Quarkus remote development allows a developer to develop the application in the same environment it will run in while having access to the same services it will have access to. Additionally, this capability greatly reduces the inner feedback loop while alleviating the "works on my machine" problem. Remote development also allows for quick and easy prototyping of new features and capabilities. Figure 4 illustrates how the remote development mode works. Figure 4: Quarkus remote dev mode incrementally synchronizes local code changes with a remote Quarkus application. First, the application is deployed to Kubernetes, a virtual machine, a container, or just some Java virtual machine (JVM) somewhere. Once running, the developer runs the remote development mode on their local machine, connecting their local machine to the remote instance. From there, development is just like live coding in Quarkus dev mode. As the developer makes code changes, Quarkus automatically compiles and pushes the changes to the remote instance. Let's continue with the Quarkus Superheroes example from before. Let's assume the entire system is deployed into a Kubernetes cluster. Let's also assume you want to make changes to the rest-fights microservice. As shown in Figure 5, you start the rest-fights microservice in remote dev mode on your local machine. The rest-fights application running on the cluster connects to the MongoDB, Apicurio Registry, and Apache Kafka instances on the Kubernetes cluster. Figure 5: Changes to the local application in remote development mode continuously send updates to the remote instance. You can then interact with the system through its user interface. Quarkus incrementally synchronizes the changes with the remote instance on the Kubernetes cluster as you make changes to the rest-fights microservice. If you want, you could even use breakpoints within your IDE on your local machine to assist with debugging. Skupper Skupper is a layer 7 service interconnect that enables secure communication across Kubernetes clusters without VPNs or special firewall rules. Using Skupper, an application can span multiple cloud providers, data centers, and regions. Figure 6 shows a high-level view of Skupper. Figure 6: Logically, Skupper connects services on different sites together to exist as a single site. With Skupper, you can create a distributed application comprised of microservices running in different namespaces within different Kubernetes clusters. Services exposed to Skupper are subsequently exposed to each namespace as if they existed in the namespace. Skupper creates proxy endpoints to make a service available within each of the namespaces where it is installed. Figure 7 shows a logical view of this architecture. Figure 7: Logically, Skupper can span multiple Kubernetes clusters and make remote services appear as local ones. Why do we mention Skupper in an article about Kubernetes native inner loop development? Because in addition to bridging applications across Kubernetes clusters, a Skupper proxy can run on any machine, enabling bidirectional communication between the machine and the other Kubernetes clusters. Logically, this is like a local machine inserted into the middle of a set of Kubernetes clusters. Services exposed to Skupper on the clusters can discover services exposed to the Skupper proxy on the local machine and vice versa. Skupper can make our Quarkus Superheroes example even more interesting, taking it further from the remote development scenario we described earlier. With Skupper, rather than continuously synchronizing changes to the rest-fights service from a local instance to a remote instance, you could completely replace the remote rest-fights instance with a local instance running Quarkus dev mode and continuous testing. Skupper would then redirect traffic on the Kubernetes cluster into the rest-fights service running on your local machine. Any outgoing requests made by the rest-fights service, such as connections to the MongoDB, Apicurio registry, and Apache Kafka instances, and even the rest-heroes and rest-villains services, would then be redirected back to the Kubernetes cluster. Figure 8 shows a logical view of what this architecture might look like. Figure 8: Logically, Skupper can make it look like a local developer machine is inside a Kubernetes cluster. You could even use Quarkus dev services to allow the rest-fights microservice to provide its own local MongoDB instance rather than using the instance on the cluster, yet continue to let traffic to Kafka flow onto the cluster. This setup would enable other Kafka consumers listening on the same topic to continue functioning. In this scenario, Quarkus continuously runs the tests of the rest-fights microservice while a developer makes live code changes, all while traffic is continually flowing through the whole system on the Kubernetes cluster. The services could even be spread out to other Kubernetes clusters on different cloud providers in other regions of the world while traffic continues to flow through a developer's local machine. A Better Developer Experience, Whether Local or Distributed Parts two and three of the previously mentioned article series at Lyft show Lyft's approach to solving this problem, albeit using different technologies. As more and more services came to life, Lyft saw that what they were doing wasn't scaling and that they, therefore, needed a kind of "remocal" environment. Quarkus was designed with many of these Developer Joy characteristics in mind. Quarkus helps developers iterate faster and contains built-in capabilities that alleviate many of these challenges and shorten the development lifecycles. Developers can focus on writing code.

By Eric Deandrea
Insight Into Developing Quarkus-Based Microservices
Insight Into Developing Quarkus-Based Microservices

Quarkus Quarkus is an open-source CDI-based framework introduced by Red Hat. It supports the development of fully reactive microservices, provides a fast startup, and has a small memory footprint. Below was our overall experience using Quarkus: It helped with a quicker and more pleasant development process. Optimized Serverless deployments for low memory usage and fast startup times Allowed us to utilize both blocking (imperative) and non-blocking (reactive) libraries and APIs Worked well with continuous testing to facilitate test-driven development Allowed support to test the JUnit test cases, which we have developed using test-driven development approach Quarkus Supports Native Builds Quarkus supports native builds for an application deployment which contains the application code, required libraries, Java APIs, and a reduced version of a VM. The smaller VM base improves the startup time of the application. To generate a native build using a Java Maven project, one can leverage Docker or podman with GraalVM: mvn clean install -Dnative -Dquarkus.native.container-build=true -Dmaven.test.skip=true The native executable is lightweight and performance optimized. Common Build and Runtime Errors Quarkus, being fairly new, lacks sufficient support in the community and documentation. Below were some of the errors/issues we encountered during development and their resolution. Build Errors UnresolvedElementException com.oracle.graal.pointsto.constraints.UnresolvedElementException: Discovered unresolved type during parsing: xxx This error is caused by missing classes at the image build time. Since the native image runtime does not include the facilities to load new classes, all code needs to be available and compiled at build time. So any class that is referenced but missing is a potential problem at run time. Solution The best practice is to provide all dependencies to the build process. If you are absolutely sure that the class is 100% optional and will not be used at run time, then you can override the default behavior of failing the build process by finding a missing class with the — allow-incomplete-classpath option to native-image. Runtime Errors Random/SplittableRandom com.oracle.graal.pointsto.constraints.UnsupportedFeatureException: Detected an instance of Random/SplittableRandom class in the image heap These errors are caused when you try to initialize these classes in a static block. Embedding instances of Random and SplittableRandom in native images cause these errors. These classes are meant to provide random values and are typically expected to get a fresh seed in each run. Embedding them in a native image results in the seed value that was generated at build-time to be cached in the native image, thus breaking that expectation. Solution We were able to resolve these by using below different ways: By avoiding build time initialization of classes holding static fields that reference (directly or transitively) instances of Random or SplittableRandomclasses. The simplest way to achieve this is to pass — initialize-at-run-time=<ClassName>to native-image and see if it works. Note that even if this works, it might impact the performance of the resulting native image since it might prevent other classes from being build-time initialized as well. Register classes holding static fields that directly reference instances of Random or SplittableRandom classes to be reinitialized at run-time. This way, the referenced instance will be re-created at run-time, solving the issue. Reset the value of fields (static or not) referencing (directly or transitively) instances of Random or SplittableRandom to null in the native-image heap. ClassNotFoundException/InstantiationException/IllegalArgumentException These errors can occur when a native image builder is not informed about some reflective calls or a resource to be loaded at run time. Or if there is a third-party/custom library that includes some, ahead-of-time incompatible code. Solution In order to resolve these exceptions, add the complaining class in reflect-config.json JSON { { "name": "com.foo.bar.Person", "allDeclaredMethods": true, "allDeclaredConstructors": true } } Reflection Issues With Native Builds When building a native executable, GraalVM operates with a closed-world assumption. Native builds with GraaVM analyzes the call tree and remove all the classes/methods/fields that are not used directly. The elements used via reflection are not part of the call tree, so they are dead code eliminated. In order to include these elements in the native executable, we’ll need to register them for reflection explicitly. JSON libraries typically use reflection to serialize the objects to JSON, and not registering these classes for reflection causes errors like the below: com.fasterxml.jackson.databind.exc.InvalidDefinitionException: No serializer found for class org.acme.jsonb.Person and no properties discovered to create BeanSerializer (to avoid an exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS) We resolved these by adding below annotation: Java @RegisterForReflection public class MyClass { ... } If the class is in a third-party jar, you can do it by using an empty class that will host the @RegisterForReflection for it: Java @RegisterForReflection(targets={ MyClassRequiringReflection.class, MySecondClassRequiringReflection.class}) public class MyReflectionConfiguration { ... } Note that MyClassRequiringReflection and MySecondClassRequiringReflection will be registered for reflection but not MyReflectionConfiguration. This feature is handy when using third-party libraries using object mapping features (such as Jackson or GSON): Java @RegisterForReflection(targets = {User.class, UserImpl.class}) public class MyReflectionConfiguration { ... } We can use a configuration file to register classes for reflection. As an example, in order to register all methods of class com.test.MyClass for reflection, we create reflection-config.json (the most common location is within src/main/resources). JSON [ { "name" : "com.test.MyClass", "allDeclaredConstructors" : true, "allPublicConstructors" : true, "allDeclaredMethods" : true, "allPublicMethods" : true, "allDeclaredFields" : true, "allPublicFields" : true } ] Integration With DynamoDB-Enhanced Client Another aspect of using Serverless architecture was the use of DynamoDB. Although there are ways to connect simple DynamoDB clients to do all operations, it does require a lot of code writing which brings verbosity and a lot of boilerplate code to the project. We considered using DynamoDBMapper but figured we couldn't use it with Quarkus since it doesn't support Java SDK1. Enhanced DynamoDB Client in Java SDK2 is the substitute Java SDK1 DynamoDBMapper, which worked well with Quarkus, although there were a few issues setting it up for classes when using native images. Annotated Java beans for creating TableSchema apparently didn’t work with native images. Mappings got lost in translation due to reflection during the native build. To resolve this, we used static table schema mappings using builder pattern, which actually is faster compared to bean annotations since it doesn't require costly bean introspection: Java TableSchema<Customer> customerTableSchema = TableSchema.builder(Customer.class) .newItemSupplier(Customer::new) .addAttribute(String.class, a -> a.name("id") .getter(Customer::getId) .setter(Customer::setId) .tags(primaryPartitionKey())) .addAttribute(Integer.class, a -> a.name("email") .getter(Customer::getEmail) .setter(Customer::setEmail) .tags(primarySortKey())) .addAttribute(String.class, a -> a.name("name") .getter(Customer::getCustName) .setter(Customer::setCustName) .addAttribute(Instant.class, a -> a.name("registrationDate") .build(); Quarkus has extensions for commonly used libraries which simplifies the use of that library in an application by providing some extra features which help in development, testing, and configuration for a native build. Recently, Quarkus released an extension for an enhanced client. This extension will resolve the above-mentioned issue related to native build and annotated Java beans for creating TableSchema caused by the use of reflection in AWS SDK. To use this extension in the Quarkus project, add the following dependency in the pom file: XML <dependency> <groupId>io.quarkiverse.amazonservices</groupId> <artifactId>quarkus-amazon-dynamodb</artifactId> </dependency> <dependency> <groupId>io.quarkiverse.amazonservices</groupId> <artifactId>quarkus-amazon-dynamodb-enhanced</artifactId> </dependency> We have three options to select an HTTP client 2 for a sync DynamoDB client and 1 for an async DynamoDB client; the default is a URL HTTP client, and for that need to import the following dependency. XML <dependency> <groupId>software.amazon.awssdk</groupId> <artifactId>url-connection-client</artifactId> </dependency> If we want an Apache HTTP client instead of a URL client, we can configure it by using the following property and dependencies: Properties files quarkus.dynamodb.sync-client.type=apache XML <dependency> <groupId>software.amazon.awssdk</groupId> <artifactId>apache-client</artifactId> </dependency> <dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-apache-httpclient</artifactId> </dependency> For an async client, the following dependency can be used: XML <dependency> <groupId>software.amazon.awssdk</groupId> <artifactId>netty-nio-client</artifactId> </dependency> Dev and test services for DynamoDB are enabled by default which uses docker to start and stop those dev services; these services help in dev and test by running a local DynamoDB instance; we can configure the following property to stop them if we don’t want to use them or don’t have Docker to run them: Properties files quarkus.dynamodb.devservices.enabled=false We can directly inject enhanced clients into our application, annotate the model with corresponding partitions, sort, and secondary partitions, and sort keys if required. Java @Inject DynamoDbEnhancedClient client; @Produces @ApplicationScoped public DynamoDbTable<Fruit> mappedTable() { return client.table("Fruit", TableSchema.fromClass(Fruit.class)) } Java @DynamoDbBean public class Fruit { private String name; private String description; @DynamoDbPartitionKey public String getName() { return name; } public void setName(String name) { this.name = name; } public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } } Achieving Security With Native Builds — SSL connections Developing microservices architecture will, at some point, require you to call a microservice from another microservice. Quarkus provides its own Rest client configuration to efficiently do so but calling those services securely (using SSL) becomes complicated when using native images. By default, Quarkus will embed the default trust store ($GRAALVM_HOME/Contents/Home/lib/security/cacerts) in the native Docker image. The function.zip generated with the native build won’t embed the default trust store, which causes issues when the function is deployed to AWS. We had to add an additional directory zip.native under src/main to add the certificates to function.zip. zip.native contains cacerts and custom bootstrap.sh (shown below) cacerts is the trust store and it holds all the certificates needed. bootstrap.sh should embed this trust store within the native function.zip. Shell # bootstrap.sh #!/usr/bin/env bash ./runner -Djava.library.path=./ -Djavax.net.ssl.trustStore=./cacerts -Djavax.net.ssl.trustStorePassword=changeit Health Checks and Fault Tolerance with Quarkus Health Checks and Fault Tolerance are crucial for microservices since these help in enabling Failover strategy in applications. We leveraged the quarkus-smallrye-health extension, which provides support to build health checks out of the box. We had overridden the HealthCheck class and added dependency health checks for dependent AWS components like DynamoDB to check for health. Below is one of the sample responses from the health checks with HTTP status 200: JSON { "status": "UP", "checks": [ { "name": "Database connections health check", "status": "UP" } ] } Along with these health checks, we used fault tolerance for microservice-to-microservice calls. In a case called microservice is down or not responding, max retry and timeouts were configured using quarks-small rye-fault-tolerance. After max retries, if the dependent service still doesn't respond, we used method fallbacks to generate static responses. Java import org.eclipse.microprofile.faulttolerance.Retry; import org.eclipse.microprofile.faulttolerance.Timeout; import org.eclipse.microprofile.faulttolerance.Fallback; ... public class FooResource { ... @GET @Retry(maxRetries = 3) @Timeout(1000) public List<Foo> getFoo() { ... } @Fallback(fallbackMethod = "fallbackRecommendations") public List<Foo> fallbackforGetFoo() { ... } public List<Foo> fallbackRecommendations() { ... return Collections.singletonList(fooRepository.getFooById(1)); } } Conclusion Overall, Quarkus is an excellent growing framework that offers a lot of options for developing serverless microservices using native images. It optimizes Java and makes it efficient for containers, cloud, and serverless environments with memory consumption optimization and a fast first response time.

By Brijendra Kumar
Styles of Software Architecture
Styles of Software Architecture

I recently started studying styles of software architecture in different ways: by reading books by renowned architects and by trying to go a step further in my professional career. I have seen how different it is to be an architect from a developer; although these two roles have many points in common, the approach is different. I don't want to describe what it means to be a software architect in this article. I will summarize what I have been reading and learning about the different styles of software architecture categorized as monolithic or distributed. Monolithic vs. Distributed Definitions When we talk about a monolithic or distributed architecture, we are referring to the type of deployment that the application has, that is, should the application be deployed as a whole, in a unitary way (monolithic), or do we deploy several isolated artifacts independently (distributed)? As a developer, I have always thought that a distributed architecture is a solution that comes after having a monolithic architecture. With the passage of time and the increase of application users, you need to isolate certain parts of your code because they make the rest collapse. I was not wrong, but it is not the only way to get there; moreover, it may be the case that you must start with some distributed architecture for different requirements such as security, elasticity, and legal issues (pe PCI), although it is not usual. Styles of Software Architecture Monolithic Architecture This software architecture style has important benefits, especially when starting an application where the domain is at a very early stage and the boundaries between contexts are very blurred. Something important to keep in mind is that regardless of how long the application has been in production, changes in the domain must be carried out. It’s not something you can leave out; we must update our implementation as often as possible to faithfully represent the domain, and no architecture will save you from this. The benefit that a monolithic software architecture style can give you is to make change faster. This does not mean that with a distributed architecture, it cannot be done; we are talking about the time it takes and the security when doing it (this implies development cost). At the development level, it also simplifies a lot in the initial stage by only talking about a single repository in a single language. At the deployment level, it is a bit more complex. At an early stage of the application, simplifying the deployment could help you a lot to focus on the domain. If you introduce microservices at this stage, you are going to have to deal not only with the domain but also with all the infrastructure that goes with it. However, as your application grows, deployments tend to be slower and spaced out over time due to security issues. For example: in e-commerce, wanting to change a small part of the product list implies a deployment not only of that part but of the rest, including the checkout system or other more vital parts, to avoid something that provides benefit being broken by a minuscule change, grouping several small changes in the same one during low-traffic hours. At the scalability level, I think the same as with the deployment; at an early stage, scale the whole project equally while you collect metrics so that later you can see which part of your project needs to scale more or less (scaling a monolith has its risks but Stack Overflow has already demonstrated that it is very viable). As time goes by, you see that it is not necessary to scale the whole project; it is likely that the product list is something that is visited much more than the checkout or the user profile page, so we could consider some changes when deploying this application (or maybe just adding an elastic, who knows). Distributed Architecture When we talk about a distributed architecture, we refer to that type of application where the modules are deployed independently, isolated from the rest, and with communication between them with remote access protocols (e.g., HTTP, grpc, queues, etc.). They are widely known due to their scalability and availability, among other factors. Performance is usually one of their strong points, especially depending on the granularity of the services. In addition, this type of architecture usually relies heavily on eventual consistency, so events are a widely used means of communication between services. This means that by not waiting for a response from the service to which we send a message, we process the requests we receive faster. Sending events opens up a wide range of possibilities for communication between services, but we still need transactions between services. This is where one of the weaknesses of distributed architectures comes in: distributed transactions. The alternative to the distributed transaction is ACID transactions, something impossible to achieve according to the way we dimension our services. In a service-based architecture or microservices where it is more domain-driven (DDD), where services are more coarse-grained, transactions will be ACID (and if they are not, it is very likely that you have a design problem). While in other architectures, for example, EDA, the transactions will be distributed, and there we will need extra pieces (e.g., SAGA, 2PC) to complete, either satisfactorily or not, the transaction. In the event that any part fails, the rest will have to be compensated. When to Use a Monolithic or a Distributed Architecture Although it is something that is asked a lot, the answer will always be “it depends," since each project will have different requirements, and depending on these requirements, a decision will be made, but it is not a decision that should be made quickly, it requires certain criteria, experience, and consideration. It is here where a developer makes the difference, and it is having the ability to see the project from another point of view, a much more global view. Developers will always tend to think more in our day-to-day and not so much in the long term; we will make decisions based on our experience and knowledge, or by mere learning, following the currents of hype. Developers who have the ability to shift their focus to look at the project will make that decision. They will define the characteristics of the application based on the business requirements, whether explicit (they are going to appear in a TV commercial) or implicit (it is an e-commerce, and you know about sales, black Friday, and so on); they will evaluate the different architectures and their tradeoffs and decide which one is more convenient. That said, as we have already mentioned, monolithic architectures are usually good, among other factors, because of the ease of change, and that is why it is a recommended architecture when you are starting a development. This is not to say that you should not consider the possibility of isolating specific components of your monolith in the future. When we usually talk about styles of software architecture, we are referring to top-level styles, that is, the architectures that define the first level. A clear example would be Modular Monolith Architecture, where at the top level, we see a monolithic component divided into modules separated by domain. Still, if we zoom into each domain, we see the layered architecture clearly identified, which would separate the layers by technical components (presentation, domain, or infrastructure, among others). Monolith or “Big Ball of Mud”? It is common to come across projects where a “monolith” is harshly criticized. When you dig a little deeper into the project, you realize that the criticism is not directed at the type of architecture but at what is known precisely as the “lack of” architecture, known as the “Big Ball Of Mud.” This leads to demonizing architecture, something that should not fall into subjectivity. The decision as to whether a monolithic architecture is good or not for the project should be made with weighty arguments, weighing all the trade-offs of both the discarded architecture and the one to be introduced. The fact that subjectively speaking, monolithic architectures have lost a lot of weight leads to complete project restructurings to move from the big ball of mud to the chosen architecture, microservices, leading us to a new architecture known as the “Distributed Big Ball Of Mud”. Needless to say, there will be teams dedicated exclusively to this, stopping business (or trying to) while such restructuring is being done. Far from fixing anything, we’ve made our system even more complicated, making it not only difficult to modify, but we’ve added many more points of failure and latency along the way. Oh, and a slightly larger DevOps team. A much better approach would be to attack the “big ball of mud” in a more direct way while delivering business value by following certain patterns that will help us move to another architecture in a healthier way. Conclusion Lack of attention to architecture leads many companies to make decisions that are not the best ones. Even though they will work, they will not work as they should. Mark Richards once said: “There are no wrong answers in architecture, only expensive ones.” And he is absolutely right: when decisions are made based on a single problem and not from a global point of view, the results are not as expected; you solve one problem by adding ten more to the list. The project will progress, but the delivery to the business will slow down little by little, causing friction between “teams” until reaching a point of no return where the delivery of value to the project will not be affordable, and a possible rewrite will have to be considered, with all that this entails. Here are some of my conclusions: Make decisions as objectively as possible, based on metrics, such as response latency or throughput, if performance is a feature to take into account or the level of coupling based on the instability of our code. Read or listen to others’ experiences through books, articles, or a simple conversation with a colleague. And for me, the most important thing is to be self-critical and leave the ego aside; most likely, the error is not in how we deploy our code but in a bad design.

By Oscar Galindo
Microservices Deployment Patterns
Microservices Deployment Patterns

Microservices are the trend of the hour. Businesses are moving towards cloud-native architecture and breaking their large applications into smaller, self-independent modules called microservices. This architecture gives a lot more flexibility, maintainability, and operability, not to mention better customer satisfaction. With these added advantages, architects and operations engineers face many new challenges as well. Earlier, they were managing one application; now they have to manage many instead. Each application again needs its own support services, like databases, LDAP servers, messaging queues, and so on. So the stakeholders need to think through different strategies for deployment where the entire application can be well deployed while maintaining its integrity and providing optimal performance. Deployment Patterns The microservices architects suggest different types of patterns that could be used to deploy microservices. Each design provides solutions for diverse functional and non-functional requirements. So microservices could be written in a variety of programming languages or frameworks. Again, they could be written in different versions of the same programming language or framework. Each microservice comprises several different service instances, like the UI, DB, and backend. The microservice must be independently deployable and scalable. The service instances must be isolated from each other. The service must be able to quickly build and deploy itself. The service must be allocated proper computing resources. The deployment environment must be reliable, and the service must be monitored. Multiple Service Instances per Host To meet the requirements mentioned at the start of this section, we can think of a solution with which we can deploy service instances of multiple services on one host. The host may be physical or virtual. So, we are running many service instances from different services on a shared host. There are different ways we could do it. We can start each instance as a JVM process. We can also start multiple instances as part of the same JVM process, kind of like a web application. We can also use scripts to automate the start-up and shutdown processes with some configurations. The configuration will have different deployment-related information, like version numbers. With this kind of approach, the resources could be used very efficiently. Service Instance per Host In many cases, microservices need their own space and a clearly separated deployment environment. In such cases, they can’t share the deployment environment with other services or service instances. There may be a chance of resource conflict or scarcity. There might be issues when services written in the same language or framework but with different versions can’t be co-located. In such cases, a service instance could be deployed on its own host. The host could either be a physical or virtual machine. In such cases, there wouldn’t be any conflict with other services. The service remains entirely isolated. All the resources of the VM are available for consumption by the service. It can be easily monitored. The only issue with this deployment pattern is that it consumes more resources. Service Instance per VM In many cases, microservices need their own, self-contained deployment environment. The microservice must be robust and must start and stop quickly. Again, it also needs quick upscaling and downscaling. It can’t share any resources with any other service. It can’t afford to have conflicts with other services. It needs more resources, and the resources must be properly allocated to the service. In such cases, the service could be built as a VM image and deployed in a VM. Scaling could be done quickly, as new VMs could be started within seconds. All VMs have their own computing resources that are properly allocated according to the needs of the microservice. There is no chance of any conflict with any other service. Each VM is properly isolated and can get support for load balancing. Service Instance per Container In some cases, microservices are very tiny. They consume very few resources for their execution. However, they need to be isolated. There must not be any resource sharing. They again can’t afford to be co-located and have a chance of conflict with another service. It needs to be deployed quickly if there is a new release. There might be a need to deploy the same service but with different release versions. The service must be capable of scaling rapidly. It also must have the capacity to start and shut down in a few milliseconds. In such a case, the service could be built as a container image and deployed as a container. In that case, the service will remain isolated. There would not be any chance of conflict. Computing resources could be allocated as per the calculated need of the service. The service could be scaled rapidly. Containers could also be started and shut down quickly. Serverless Deployment In certain cases, the microservice might not need to know the underlying deployment infrastructure. In these situations, the deployment service is contracted out to a third-party vendor, who is typically a cloud service provider. The business is absolutely indifferent about the underlying resources; all it wants to do is run the microservice on a platform. It pays the service provider based on the resources consumed from the platform for each service call. The service provider picks the code and executes it for each request. The execution may happen in any executing sandbox, like a container, VM, or whatever. It is simply hidden from the service itself. The service provider takes care of provisioning, scaling, load-balancing, patching, and securing the underlying infrastructure. Many popular examples of serverless offerings include AWS Lambda, Google Functions, etc. The infrastructure of a serverless deployment platform is very elastic. The platform scales the service to absorb the load automatically. The time spent managing the low-level infrastructure is eliminated. The expenses are also lowered as the microservices provider pays only for the resources consumed for each call. Service Deployment Platform Microservices can also be deployed on application deployment platforms. By providing some high-level services, such platforms clearly abstract out the deployment. The service abstraction can handle non-functional and functional requirements like availability, load balancing, monitoring, and observability for the service instances. The application deployment platform is thoroughly automated. It makes the application deployment quite reliable, fast, and efficient. Examples of such platforms are Docker Swarm, Kubernetes, and Cloud Foundry, which is a PaaS offering. Conclusion Microservices deployment options and offerings are constantly evolving. It's possible that a few more deployment patterns will follow suit. Many of these patterns mentioned above are very popular and are being used by most microservice providers. They are very successful and reliable. But with changing paradigms, administrators are thinking of innovative solutions.

By Aditya Bhuyan
How I Used Render To Scale My Microservices App With Ease
How I Used Render To Scale My Microservices App With Ease

As a software lead and feature developer, I witnessed a team learn an important lesson about resource scaling and the cloud. Spoiler alert: the lesson was that if you’re not careful, scaling can get expensive! The team felt they had no choice but to keep increasing the number of instances for a given service that was under heavy load. When they finished scaling up, the number of instances was several times larger than the number of instances that were configured by default. What they didn’t realize at the time was that—despite the load returning back to a normal state—their extra instances remained in place. Everyone seemed to be okay with this “scale up as much as needed” approach… until they received their next invoice from the cloud provider. This situation got me thinking about Render, a platform I’ve been adopting more and more for some of my projects. It made me wonder just how easy it could be to implement scaling in cloud-based applications and services with Render. Another spoiler alert: it’s easy. The Concept of Cloud-Based Scaling Consumers of your application or service have one common expectation: all of their requests should be handled in a reasonable amount of time. At the same time, solution owners have expectations, which include: Ensuring that customer expectations are met Keeping costs within a planned budget Minimizing downtime and outages—especially those related to performance All of these expectations are easy to meet when the level of demand is less than the max capacity of the technology used to handle each request. When demand begins to exceed those levels, that’s when things get interesting. The challenge is finding that sweet spot that meets expectations and keeps costs reasonable. That’s where the concept of cloud-based scaling comes into play. With cloud-based scaling, the focus is on scaling up the number of service instances to meet the current demand but scaling back down when the demand subsides. A Trio of Scenarios There are three use cases for auto-scaling that we will talk about: Manual scaling Auto-scaling up Auto-scaling down Let’s explore each use case with a scenario example. Manual Scaling The manual scaling concept is for teams with a strong understanding of the demand for their applications or services. As an example, consider an income-tax-related service that answers customers’ questions as they populate their tax returns. The team supporting this service may have decades of information regarding usage patterns, allowing them to determine how many service instances are required throughout the entire year. With this information in hand, the manual scaling approach will allow consumers to be satisfied because the team always knows how many instances ought to be available. Solution owners are pleased because their monthly spend is completely on budget. Of course, this information does not consider major changes to the expected usage patterns. For example, maybe a press release on the service comes out, suddenly impacting demand positively or negatively. Auto-Scaling Up The auto-scaling up approach puts the number of instances into the hands of pre-defined thresholds created by the service owner but enforced by the cloud provider. As those thresholds are exceeded, the number of instances will grow until demand falls to an expected level. Most providers allow users to set a maximum number of instances to cap the number of instances which can ultimately be spawned. While there is some uncertainty regarding the impact on the monthly budget, solution owners might use the rationale that increased demand for their service is often related to new or upgraded subscriptions, which result in additional income. This is where the concept of “you have to spend money to make money” comes into play. When implementing auto-scaling up policies, I always recommend doing the same for auto-scaling down, too. Auto-Scaling Down The auto-scaling down approach is similar to auto-scaling up, except the number of services decreases to coincide with a reduction in demand. While the auto-scaling up functionality is very quick to introduce new instances, auto-scaling down is often delayed to avoid scaling down too soon. Thinking back to the team mentioned in my introduction, had they employed auto-scaling down for the service I mentioned, they would not have encountered the sticker shock of leaving all of those instances running well after the peak demand had subsided. Cloud providers that offer auto-scaling are now getting into the practice of combining auto-scaling up with auto-scaling down since this is the more common implementation of this functionality. Scaling with Render I have written about the Render platform several times this year. Here are links to some of my other publications on the subject: Using Render and Go for the First Time Under the Hood: Render Unified Cloud Purpose-Driven Microservice Design Launch Your Startup Idea in a Day I’ve learned that they take their Zero DevOps promise very seriously. As one might expect, scaling with Render is easy and driven by a simple user interface. With a service running at the Starter plan (or above), the ability to manually scale the number of instances is as simple as sliding to the required level in the Scaling menu of the Render Dashboard: If you’re interested in using auto-scaling with Render, simply enable Autoscaling, then: Select the number of instances Enable and set a target CPU utilization Enable and set a target memory utilization Keep in mind: it is possible to limit auto-scaling to depend on only CPU or memory utilization (instead of both). After implementing auto-scaling, the Render Dashboard communicates as changes are made to the number of instances running: Additionally, metrics are provided to justify the auto-scaling implementation: From a billing perspective, changes to the cost structure are based upon the amount of time the new instances are in place in a given month. This means if you double your number of instances for seven hours on a single day of the billing cycle, the cost for that billing cycle will not double; instead, it will only double for those seven hours where the number of instances doubled. Other Available Integrations Services deployed with Render can also integrate with the following solutions: Datadog: Provides Postgres metrics and log streams into the Datadog observatory platform Scout APM: Provides application performance monitoring (APM) for Ruby, PHP, Python, Node.js, and Elixir-based services These integrations provide insights, which can be helpful to larger, enterprise-scale applications and solutions running on the Render platform. Conclusion Technologists who have worked for less than 13 years have been fortunate not to have to worry about the side effects of a global recession. Present-day economists suggest that the next recession will begin soon, and some economic indicators already justify such claims. This means that corporations are likely to be more conservative with their spending to preserve their bottom line. One area of scrutiny for corporations is cloud spending. I still believe that cloud-based products and services can strongly outweigh the costs to support and maintain similar configurations within a dedicated data center. That said, there are certain aspects that can significantly impact the periodic costs related to cloud-based technology: Having a good understanding of each incurred cost Knowing how and when to scale applications and services to meet demand For those using cloud services from Amazon, Google, or Microsoft, firms like CleanSlate Technology Group offer services to help you with these concerns. Since 2021, I have been trying to live by the following mission statement, which I feel can apply to any technology professional: “Focus your time on delivering features/functionality that extends the value of your intellectual property. Leverage frameworks, products, and services for everything else.” - J. Vester In the time I have been utilizing Render for my own applications and services, I have been able to focus on delivering features strongly because of its Zero DevOps model. For those looking to simplify their cloud architecture, Render provides mission-critical scalability without becoming an expert in the technologies adopted by their competitors. Have a really great day!

By John Vester CORE
Microservices Orchestration
Microservices Orchestration

This is an article from DZone's 2022 Microservices and Containerization Trend Report.For more: Read the Report Does your organization use a microservices-style architecture to implement its business functionality? What approaches to microservices communication and orchestration do you use? Microservices have been a fairly dominant application architecture for the last few years and are usually coupled with the adoption of a cloud platform (e.g., containers, Kubernetes, FaaS, ephemeral cloud services). Communication patterns between these types of services vary quite a bit. Microservices architectures stress independence and the ability to change frequently, but these services often need to share data and initiate complex interactions between themselves to accomplish their functionality. In this article, we'll take a look at patterns and strategies for microservices communication. Problems in the Network Communicating over the network introduces reliability concerns. Packets can be dropped, delayed, or duplicated, and all of this can contribute to misbehaving and unreliable service-to-service communication. In the most basic case — service A opening a connection to service B — we put a lot of trust in the application libraries and the network itself to open a connection and send a request to the target service (service B in this case). Figure 1: Simple example of service A calling service B But what happens if that connection takes too long to open? What if that connection times out and cannot be open? What if that connection succeeds but then later gets shut down after processing a request, but before a response? We need a way to quickly detect connection or request issues and decide what to do. Maybe if service A cannot communicate with service B, there is some reasonable fallback (e.g., return an error message, static response, respond with a cached value). Figure 2: More complicated example of calling multiple services In a slightly more complicated case, service A might need to call service B, retrieve some values from the service B response, and use it to call service C. If the call to service B succeeds but the call to service C fails, the fallback option may be a bit more complicated. Maybe we can fall back to a predefined response, retry the request, pull from a cache based on some of the data from the service B response, or maybe call a different service? Problems within the network that cause connection or request failures can, and do, happen intermittently and must be dealt with by the applications. These problems become more likely and more complicated with the more service calls orchestrated from a given service, as is seen in Figure 3. Figure 3: Trying to orchestrate multiple service calls across read/write APIs These problems become even more troubling when these calls between services aren't just "read" calls. For example, if service A calls service B, which performs some kind of data mutation that must be coordinated with the next call to service C (e.g., service A tells service B that customer Joe’s address is updated but must also tell service C to change the shipping because of the address change), then these failed calls are significant. This can result in inconsistent data and an inconsistent state across services. Network errors like this impact resilience, data consistency, and likely service-level objectives (SLOs) and service-level agreements (SLAs). We need a way to deal with these network issues while taking into account other issues that crop up when trying to account for failures. Helpful Network Resilience Patterns Building APIs and services to be resilient to network unreliability is not always easy. Services (including the frameworks and libraries used to build a service) can fail due to the network in sometimes unpredictable ways. A few patterns that go a long way to building resilient service communication are presented here but are certainly not the only ones. These three patterns can be used as needed or together to improve communication reliability (but each has its own drawbacks): Retry/backoff retry – if a call fails, send the request again, possibly waiting a period of time before trying again Idempotent request handling – the ability to handle a request multiple times with the same result (can involve de-duplication handling for write operations) Asynchronous request handling – eliminating the temporal coupling between two services for request passing to succeed Let’s take a closer look at each of these patterns. Retries With Backoff Handling Network unreliability can strike at any time and if a request fails or a connection cannot be established, one of the easiest things to do is retry. Typically, we need some kind of bounded number of retries (e.g., "retry two times" vs. just retry indefinitely) and potentially a way to backoff the retries. With backoffs, we can stagger the time we spend between a call that fails and how long to retry. One quick note about retries: We cannot just retry forever, and we cannot configure every service to retry the same number of times. Retries can contribute negatively to "retry storm" events where services are degrading and the calling services are retrying so much that it puts pressure on, and eventually takes down, a degraded service (or keeps it down as it tries to come back up). A starting point could be to use a small, fixed number of retries (say, two) higher up in a call chain and don’t retry the deeper you get into a call chain. Idempotent Request Handling Idempotent request handling is implemented on the service provider for services that make changes to data based on an incoming request. A simple example would be a counter service that keeps a running total count and increments the count based on requests that come in. For example, a request may come in with the value "5," and the counter service would increment the current count by 5. But what if the service processes the request (increments of 5), but somehow the response back to the client gets lost (network drops the packets, connection fails, etc.)? The client may retry the request, but this would then increment the count by 5 again, and this may not be the desired state. What we want is the service to know that it’s seen a particular request already and either disregard it or apply a "no-op." If a service is built to handle requests idempotently, a client can feel safe to retry the failed request with the service able to filter out those duplicate requests. Asynchronous Request Handling For the service interactions in the previous examples, we've assumed some kind of request/response interaction, but we can alleviate some of the pains of the network by relying on some kind of queue or log mechanism that persists a message in flight and delivers it to consumers. In this model, we remove the temporal aspect of both a sender and a receiver of a request being available at the same time. We can trust the message log or queue to persist and deliver the message at some point in the future. Retry and idempotent request handling are also applicable in the asynchronous scenario. If a message consumer can correctly apply changes that may occur in an "at-least once delivery" guarantee, then we don't need more complicated transaction coordination. Essential Tools and Considerations for Service-to-Service Communication To build resilience into service-to-service communication, teams may rely on additional platform infrastructure, for example, an asynchronous message log like Kafka or a microservices resilience framework like Istio service mesh. Handling tasks like retries, circuit breaking, and timeouts can be configured and enforced transparently to an application with a service mesh. Since you can externally control and configure the behavior, these behaviors can be applied to any/all of your applications — regardless of the programming language they've been written in. Additionally, changes can be made quickly to these resilience policies without forcing code changes. Another tool that helps with service orchestration in a microservices architecture is a GraphQL engine. GraphQL engines allow teams to fan out and orchestrate service calls across multiple services while taking care of authentication, authorization, caching, and other access mechanisms. GraphQL also allows teams to focus more on the data elements of a particular client or service call. GraphQL started primarily for presentation layer clients (web, mobile, etc.) but is being used more and more in service-to-service API calls as well. Figure 4: Orchestrating service calls across multiple services with a GraphQL engine GraphQL can also be combined with API Gateway technology or even service mesh technology, as described above. Combining these provides a common and consistent resilience policy layer — regardless of what protocols are being used to communicate between services (REST, gRPC, GraphQL, etc.). Conclusion Most teams expect a cloud infrastructure and microservices architecture to deliver big promises around service delivery and scale. We can set up CI/CD, container platforms, and a strong service architecture, but if we don’t account for runtime microservices orchestration and the resilience challenges that come with it, then microservices are really just an overly complex deployment architecture with all of the drawbacks and none of the benefits. If you’re going down a microservices journey (or already well down the path), make sure service communication, orchestration, security, and observability are at front of mind and consistently implemented across your services. This is an article from DZone's 2022 Microservices and Containerization Trend Report.For more: Read the Report

By Christian Posta
Architectural Patterns for Microservices With Kubernetes
Architectural Patterns for Microservices With Kubernetes

This is an article from DZone's 2022 Kubernetes in the Enterprise Trend Report.For more: Read the Report For some time, microservices have drawn interest across the architecture and software engineering landscape, and now, applications comprised of microservices have become commonplace. So what exactly is the definition of a microservice? That is somewhat of a loaded question as there is plenty of debate on granularity, segmentation, and what designates a microservice. For the purposes of this discussion, a microservices-based architecture is segmenting an application's units of work into discrete, interoperable components. This is a broad definition, but it is workable in that it identifies two foundational microservice concepts: discrete and interoperable. Along with the technical and business benefits, a microservices-based application architecture brings its own set of challenges. These challenges have been met with solutions ranging from new architectural patterns to the evolution of tech stacks themselves. Kubernetes has become one of the technologies in the tech stack evolution. Deploying microservices using Kubernetes enhances and enforces key principles and patterns while offering additional benefits. It's an Evolution, Not a Revolution As with any technical evolution, taking the next step improves upon what has already been shown to be successful while removing barriers to adoption or execution. Kubernetes is not going to address all microservices challenges, but it does address several pain points. Best Practices Remain In many cases, the development and packaging of microservices destined for Kubernetes deployment is no different than a non-Kubernetes deployment. Non-Kubernetes deployments include bare metal servers, virtual machines, and containerized applications. Applications already packaged for containerized deployment make the step to adopt Kubernetes-managed microservices straightforward. All key microservices patterns, development, and deployment best practices are applied. Application tech stacks and components are unchanged. Continuous integration/continuous delivery (deployment) systems remain intact. Operating system platforms and versions can be tightly controlled. Differences The differences between Kubernetes and non-Kubernetes microservices architectures focus less on the task performed by the microservices and more on the deployment of non-functional requirements. Satisfying non-functional requirements is not a new concept introduced by Kubernetes or even by a microservices architecture. However, through a combination of leveraging the services offered by Kubernetes itself as well as defining cross-cutting application support services, supporting many nonfunctional requirements becomes transparent to an application. The following are two examples. Kubernetes Ingress A Kubernetes Ingress is an example of a configurable service that auto-configures external access to microservices. When a microservice is deployed, it can define whether and how it is to be externally accessed. If a microservice specifies that it is to be externally accessible, the Ingress services within the Kubernetes cluster automatically configure external access, including details such as virtual host definitions and SSL certificates. Figure 1: An Ingress definition supporting two services Here, a Kubernetes Ingress accepts HTTP(S) requests external to the Kubernetes cluster and, based on the request path, routes requests to specific services within the cluster. Operators Kubernetes Operators are a Cloud Native Computing Foundation (CNCF) specification outlining a pattern that supports cross-cutting application services. They behave similarly to a Kubernetes Ingress in that a service is auto-configured based on application specification. The primary difference is that Kubernetes Operators present an abstraction where any type of service is automatically configured to extend the behavior of a Kubernetes cluster. There are Kubernetes Operators that connect applications to logging and metrics systems with the application knowing little of the specifics regarding those systems' implementation. There are also Kubernetes Operators that will build and deploy complete database instances. Figure 2: Kubernetes Operator flow In the diagram above, an application requests that a service be made available for its use. The Kubernetes Operator monitors and watches for requests. When a request is made, the Kubernetes Operator instructs the Kubernetes cluster to deploy or configure a cross-cutting service specific to the application's request. Abstractions Kubernetes provides and supports abstractions over many systems required to satisfy non-functional components. Successful Kubernetes microservices architectures are comprehensive beyond application architecture, considering a strategy to not only address interoperability across microservices but coordination with common services. Applying Kubernetes Constructs to a Microservices Architecture Kubernetes deploys container-based applications; this implies that an artifact of a microservice build and packaging process is a Docker (or suitable alternative) image. In Kubernetes, the basic deployment unit for an image is a Pod. Often there is a one-to-one relationship between a deployed image and a Pod. However, Kubernetes Pods can support multiple deployed images within a single Pod. While the deployed containers do not share a file system, they can reference each other using localhost. Within a Kubernetes cluster, deployed Pods can provide their services to other Pods. This is like a deployed microservice on bare metal or a virtual machine, although this deployment doesn't provide access to the Pod's service from an external resource, high availability, or scalability. As discussed, Kubernetes helps applications meet non-functional requirements. A general rule of thumb is when "-ility" is used to describe a function, it often means a non-functional requirement. Using high availability and scalability as examples, Kubernetes provides these with relative ease. There are a few Kubernetes constructs that support these functions. Two are presented here: Services and Deployments. Kubernetes provides a construct called a Service. A Kubernetes Service specifies ports that a microservice wishes to expose and how they are to be exposed. Services provide two powerful features. First, a Kubernetes Service integrates with the internal Kubernetes DNS service to provide a consistent hostname by which the microservices are accessed within the Kubernetes cluster. In addition, if there are multiple instances of the same microservice Pod, a Kubernetes Service can act as a load balancer across the Pod instances, providing high availability. While Pod instances can be individually deployed, manually monitoring their status is impractical. A common pattern for adding automation to Pod "-ilities" is Kubernetes Deployments. Kubernetes Deployments specify details surrounding Pod definitions and provide several features that support the production deployment of microservices, including: The number of replicas to be maintained Updating the state of declared Pods Rollback to earlier versions Scaling up or down the number of Pods With Pod, Service, and Deployment definitions, a solid microservices architecture is in place. In this microcosm, one piece remains — that is, auto-scaling. With Deployments, scalability is available, but like direct Pod deployments, they are manually controlled. The final component to this architectural pattern is using a HorizontalPodAutoscaler to automatically scale the number of Pod instances based on certain criteria (e.g., CPU usage). This example demonstrates how Kubernetes can take any containerized microservice and, using Kubernetes constructs, satisfy the critical non-functional requirements that most applications require. Assembling the patterns discussed here, the following diagram presents a high-level visual of a Kubernetes microservices deployment pattern: Figure 3: Putting it all together The diagram portrays two microservices, "greensvc" and "bluesvc." Each microservice utilizes a Kubernetes Service to expose its functionality. In addition to providing high availability by load balancing multiple Kubernetes Pods per microservice, the Kubernetes Service maps expose Pod ports to port 80. The definition of a Kubernetes Service also creates DNS entries internal to the Kubernetes cluster (greensvc.ns.cluster.local and bluesvc.ns.cluster.local) that can allow microservices to interoperate. Both microservices are exposed outside the Kubernetes cluster through a Kubernetes Ingress. The configured ingress routes incoming requests to their respective services. Microservices Deployment Patterns Kubernetes provides many constructs and abstractions to support service and application Deployment. While applications differ, there are foundational concepts that help drive a well-defined microservices deployment strategy. Well-designed microservices deployment patterns play into an often-overlooked Kubernetes strength. Kubernetes is independent of runtime environments. Runtime environments include Kubernetes clusters running on cloud providers, in-house, bare metal, virtual machines, and developer workstations. When Kubernetes Deployments are designed properly, deploying to each of these and other environments is accomplished with the same exact configuration. In grasping the platform independence offered by Kubernetes, developing and testing the deployment of microservices can begin with the development team and evolve through to production. Each iteration contributes to the overall deployment pattern. A production deployment definition is no different than a developer's workstation configuration. This pattern provides a level of validation that is difficult to reproduce in any previous pattern and can lead to rapid maturity of an application's delivery cycle. The Kubernetes ecosystem offers tools that support these patterns. The most predominant tool is Helm, which orchestrates the definition, installation, and upgrade of Kubernetes applications. It's through tools such as Helm that the same deployment definition can be executed across multiple runtime environments by simply supplying a set of parameters specific to a runtime environment. These parameters don't change the deployment pattern; rather, they configure the deployment pattern to meet the runtime environment (e.g., configuring the amount of memory to allocate to a process). To learn more about Helm charts, check out the article, "Advanced Guide to Helm Charts for Package Management in Kubernetes." Microservices Deployment in Kubernetes Makes Sense Deploying microservices in Kubernetes is an evolution of microservices architectures. Kubernetes addresses many pain points and challenges in developing and deploying microservices-based applications. Being an evolution implies that it's not a revolution. It's not a rewrite. When designing microservices, in many ways, Kubernetes addresses the question that needs to be answered. Rather than waiting, good Kubernetes design and deployment patterns encourage tackling non-functional requirements early in the development process, leading to an application that will mature much faster. Whether it's Kubernetes or a different deployment platform, the same issues that need to be considered will need to be addressed upfront or later. In software engineering, it's almost always best to consider issues upfront. Kubernetes directly helps in addressing many microservices architectures and deployment challenges. This is an article from DZone's 2022 Kubernetes in the Enterprise Trend Report.For more: Read the Report

By Ray Elenteny CORE

Top Microservices Experts

expert thumbnail

Nuwan Dias

VP and Deputy CTO,
WSO2

‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎
expert thumbnail

Christian Posta

MVB,
DZone MVB

Christian Posta (@christianposta) is Field CTO at solo.io and well known in the community for being an author (Istio in Action, Manning, Microservices for Java Developers, O’Reilly 2016), frequent blogger, speaker, open-source enthusiast and committer on various open-source projects including Istio and Kubernetes. Christian has spent time at web-scale companies and now helps companies create and deploy large-scale, resilient, distributed architectures - many of what we now call Serverless and Microservices. He enjoys mentoring, training and leading teams to be successful with distributed systems concepts, microservices, devops, and cloud-native application design.
expert thumbnail

Rajesh Bhojwani

Development Architect,
Sap Labs

You can reach out to me at rajesh.bhojwani@gmail.com or https://www.linkedin.com/in/rajesh-bhojwani
expert thumbnail

Ray Elenteny

Solution Architect,
SOLTECH

‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎

The Latest Microservices Topics

article thumbnail
Real-Time Stream Processing With Hazelcast and StreamNative
In this article, readers will learn about real-time stream processing with Hazelcast and StreamNative in a shorter time, along with demonstrations and code.
January 27, 2023
by Timothy Spann
· 1,887 Views · 2 Likes
article thumbnail
DevOps Roadmap for 2022
[Originally published February 2022] In this post, I will share some notes from my mentoring session that can help you - a DevOps engineer or platform engineer, learn where to focus.
January 26, 2023
by Anjul Sahu
· 17,972 Views · 6 Likes
article thumbnail
What Is Policy-as-Code? An Introduction to Open Policy Agent
Learn the benefits of policy as code and start testing your policies for cloud-native environments.
January 26, 2023
by Tiexin Guo
· 3,065 Views · 1 Like
article thumbnail
Key Considerations When Implementing Virtual Kubernetes Clusters
In this article, readers will receive key considerations to examine when implementing virutal Kubernetes clusters, along with essential questions and answers.
January 25, 2023
by Hanumantha (Hemanth) Kavuluru
· 3,103 Views · 3 Likes
article thumbnail
How Do the Docker Client and Docker Servers Work?
This article will help you deeply understand how Docker's client-server model works and give you more insights about the Docker system.
January 25, 2023
by Eugenia Kuzmenko
· 3,063 Views · 1 Like
article thumbnail
Revolutionizing Supply Chain Management With AI: Improving Demand Predictions and Optimizing Operations
How are AI and ML being used to revolutionize supply chain management? What are the latest advancements and best practices?
January 25, 2023
by Frederic Jacquet CORE
· 1,947 Views · 1 Like
article thumbnail
A Brief Overview of the Spring Cloud Framework
Readers will get an overview of the Spring Cloud framework, a list of its main packages, and their relation with the Microservice Architectural patterns.
January 25, 2023
by Mario Casari
· 4,876 Views · 1 Like
article thumbnail
Spring Cloud: How To Deal With Microservice Configuration (Part 1)
In this article, we cover how to use a Spring Cloud Configuration module to implement a minimal microservice scenario based on a remote configuration.
January 24, 2023
by Mario Casari
· 3,877 Views · 2 Likes
article thumbnail
How To Check Docker Images for Vulnerabilities
Regularly checking for vulnerabilities in your pipeline is very important. One of the steps to execute is to perform a vulnerability scan of your Docker images. In this blog, you will learn how to perform the vulnerability scan, how to fix the vulnerabilities, and how to add it to your Jenkins pipeline. Enjoy! 1. Introduction In a previous blog from a few years ago, it was described how you could scan your Docker images for vulnerabilities. A follow-up blog showed how to add the scan to a Jenkins pipeline. However, Anchore Engine, which was used in the previous blogs, is not supported anymore. An alternative solution is available with grype, which is also provided by Anchore. In this blog, you will take a closer look at grype, how it works, how you can fix the issues, and how you can add it to your Jenkins pipeline. But first of all, why check for vulnerabilities? You have to stay up-to-date with the latest security fixes nowadays. Many security vulnerabilities are publicly available and therefore can be exploited quite easily. It is therefore a must-have to fix security vulnerabilities as fast as possible in order to minimize your attack surface. But how do you keep up with this? You are mainly focused on business and do not want to have a full-time job fixing security vulnerabilities. That is why it is important to scan your application and your Docker images automatically. Grype can help with scanning your Docker images. Grype will check operating system vulnerabilities but also language-specific packages such as Java JAR files for vulnerabilities and will report them. This way, you have a great tool that will automate the security checks for you. Do note that grype is not limited to scanning Docker images. It can also scan files and directories and can therefore be used for scanning your sources. In this blog, you will create a vulnerable Docker image containing a Spring Boot application. You will install and use grype in order to scan the image and fix the vulnerabilities. In the end, you will learn how to add the scan to your Jenkins pipeline. The sources used in this blog can be found on GitHub. 2. Prerequisites The prerequisites needed for this blog are: Basic Linux knowledge Basic Docker knowledge Basic Java and Spring Boot knowledge 3. Vulnerable Application Navigate to Spring Initializr and choose a Maven build, Java 17, Spring Boot 2.7.6, and the Spring Web dependency. This will not be a very vulnerable application because Spring already ensures that you use the latest Spring Boot version. Therefore, change the Spring Boot version to 2.7.0. The Spring Boot application can be built with the following command, which will create the jar file for you: Shell $ mvn clean verify You are going to scan a Docker image, therefore a Dockerfile needs to be created. You will use a very basic Dockerfile which just contains the minimum instructions needed to create the image. If you want to create production-ready Docker images, do read the posts Docker Best Practices and Spring Boot Docker Best Practices. Dockerfile FROM eclipse-temurin:17.0.1_12-jre-alpine WORKDIR /opt/app ARG JAR_FILE COPY target/${JAR_FILE} app.jar ENTRYPOINT ["java", "-jar", "app.jar"] At the time of writing, the latest eclipse-temurin base image for Java 17 is version 17.0.5_8. Again, use an older one in order to make it vulnerable. For building the Docker image, a fork of the dockerfile-maven-plugin of Spotify will be used. The following snippet is therefore added to the pom file. XML com.xenoamess.docker dockerfile-maven-plugin 1.4.25 mydeveloperplanet/mygrypeplanet ${project.version} ${project.build.finalName}.jar The advantage of using this plugin is that you can easily reuse the configuration. Creating the Docker image can be done by a single Maven command. Building the Docker image can be done by invoking the following command: Shell $ mvn dockerfile:build You are now all set up to get started with grype. 4. Installation Installation of grype can be done by executing the following script: Shell $ curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b /usr/local/bin Verify the installation by executing the following command: Shell $ grype version Application: grype Version: 0.54.0 Syft Version: v0.63.0 BuildDate: 2022-12-13T15:02:51Z GitCommit: 93499eec7e3ce2704755e9f51457181b06b519c5 GitDescription: v0.54.0 Platform: linux/amd64 GoVersion: go1.18.8 Compiler: gc Supported DB Schema: 5 5. Scan Image Scanning the Docker image is done by calling grype followed by docker:, indicating that you want to scan an image from the Docker daemon, the image, and the tag: Shell $ grype docker:mydeveloperplanet/mygrypeplanet:0.0.1-SNAPSHOT Application: grype Version: 0.54.0 Syft Version: v0.63.0 Vulnerability DB [updated] Loaded image Parsed image Cataloged packages [50 packages] Scanned image [42 vulnerabilities] NAME INSTALLED FIXED-IN TYPE VULNERABILITY SEVERITY busybox 1.34.1-r3 1.34.1-r5 apk CVE-2022-28391 High jackson-databind 2.13.3 java-archive CVE-2022-42003 High jackson-databind 2.13.3 java-archive CVE-2022-42004 High jackson-databind 2.13.3 2.13.4 java-archive GHSA-rgv9-q543-rqg4 High jackson-databind 2.13.3 2.13.4.1 java-archive GHSA-jjjh-jjxp-wpff High java 17.0.1+12 binary CVE-2022-21248 Low java 17.0.1+12 binary CVE-2022-21277 Medium java 17.0.1+12 binary CVE-2022-21282 Medium java 17.0.1+12 binary CVE-2022-21283 Medium java 17.0.1+12 binary CVE-2022-21291 Medium java 17.0.1+12 binary CVE-2022-21293 Medium java 17.0.1+12 binary CVE-2022-21294 Medium java 17.0.1+12 binary CVE-2022-21296 Medium java 17.0.1+12 binary CVE-2022-21299 Medium java 17.0.1+12 binary CVE-2022-21305 Medium java 17.0.1+12 binary CVE-2022-21340 Medium java 17.0.1+12 binary CVE-2022-21341 Medium java 17.0.1+12 binary CVE-2022-21360 Medium java 17.0.1+12 binary CVE-2022-21365 Medium java 17.0.1+12 binary CVE-2022-21366 Medium libcrypto1.1 1.1.1l-r7 apk CVE-2021-4160 Medium libcrypto1.1 1.1.1l-r7 1.1.1n-r0 apk CVE-2022-0778 High libcrypto1.1 1.1.1l-r7 1.1.1q-r0 apk CVE-2022-2097 Medium libretls 3.3.4-r2 3.3.4-r3 apk CVE-2022-0778 High libssl1.1 1.1.1l-r7 apk CVE-2021-4160 Medium libssl1.1 1.1.1l-r7 1.1.1n-r0 apk CVE-2022-0778 High libssl1.1 1.1.1l-r7 1.1.1q-r0 apk CVE-2022-2097 Medium snakeyaml 1.30 java-archive GHSA-mjmj-j48q-9wg2 High snakeyaml 1.30 1.31 java-archive GHSA-3mc7-4q67-w48m High snakeyaml 1.30 1.31 java-archive GHSA-98wm-3w3q-mw94 Medium snakeyaml 1.30 1.31 java-archive GHSA-c4r9-r8fh-9vj2 Medium snakeyaml 1.30 1.31 java-archive GHSA-hhhw-99gj-p3c3 Medium snakeyaml 1.30 1.32 java-archive GHSA-9w3m-gqgf-c4p9 Medium snakeyaml 1.30 1.32 java-archive GHSA-w37g-rhq8-7m4j Medium spring-core 5.3.20 java-archive CVE-2016-1000027 Critical ssl_client 1.34.1-r3 1.34.1-r5 apk CVE-2022-28391 High zlib 1.2.11-r3 1.2.12-r0 apk CVE-2018-25032 High zlib 1.2.11-r3 1.2.12-r2 apk CVE-2022-37434 Critical What does this output tell you? NAME: The name of the vulnerable package INSTALLED: Which version is installed FIXED-IN: In which version the vulnerability is fixed TYPE: The type of dependency, e.g., binary for the JDK, etc. VULNERABILITY: The identifier of the vulnerability; with this identifier, you are able to get more information about the vulnerability in the CVE database SEVERITY: Speaks for itself and can be negligible, low, medium, high, or critical. As you take a closer look at the output, you will notice that not every vulnerability has a confirmed fix. So what do you do in that case? Grype provides an option in order to show only the vulnerabilities with a confirmed fix. Adding the --only-fixed flag will do the trick. Shell $ grype docker:mydeveloperplanet/mygrypeplanet:0.0.1-SNAPSHOT --only-fixed Vulnerability DB [no update available] Loaded image Parsed image Cataloged packages [50 packages] Scanned image [42 vulnerabilities] NAME INSTALLED FIXED-IN TYPE VULNERABILITY SEVERITY busybox 1.34.1-r3 1.34.1-r5 apk CVE-2022-28391 High jackson-databind 2.13.3 2.13.4 java-archive GHSA-rgv9-q543-rqg4 High jackson-databind 2.13.3 2.13.4.1 java-archive GHSA-jjjh-jjxp-wpff High libcrypto1.1 1.1.1l-r7 1.1.1n-r0 apk CVE-2022-0778 High libcrypto1.1 1.1.1l-r7 1.1.1q-r0 apk CVE-2022-2097 Medium libretls 3.3.4-r2 3.3.4-r3 apk CVE-2022-0778 High libssl1.1 1.1.1l-r7 1.1.1n-r0 apk CVE-2022-0778 High libssl1.1 1.1.1l-r7 1.1.1q-r0 apk CVE-2022-2097 Medium snakeyaml 1.30 1.31 java-archive GHSA-3mc7-4q67-w48m High snakeyaml 1.30 1.31 java-archive GHSA-98wm-3w3q-mw94 Medium snakeyaml 1.30 1.31 java-archive GHSA-c4r9-r8fh-9vj2 Medium snakeyaml 1.30 1.31 java-archive GHSA-hhhw-99gj-p3c3 Medium snakeyaml 1.30 1.32 java-archive GHSA-9w3m-gqgf-c4p9 Medium snakeyaml 1.30 1.32 java-archive GHSA-w37g-rhq8-7m4j Medium ssl_client 1.34.1-r3 1.34.1-r5 apk CVE-2022-28391 High zlib 1.2.11-r3 1.2.12-r0 apk CVE-2018-25032 High zlib 1.2.11-r3 1.2.12-r2 apk CVE-2022-37434 Critical Note that the vulnerabilities for the Java JDK have disappeared, although there exists a more recent update for the Java 17 JDK. However, this might not be a big issue, because the other (non-java-archive) vulnerabilities show you that the base image is outdated. 6. Fix Vulnerabilities Fixing the vulnerabilities is quite easy in this case. First of all, you need to update the Docker base image. Change the first line in the Docker image: Dockerfile FROM eclipse-temurin:17.0.1_12-jre-alpine into: Dockerfile FROM eclipse-temurin:17.0.5_8-jre-alpine Build the image and run the scan again: Shell $ mvn dockerfile:build ... $ grype docker:mydeveloperplanet/mygrypeplanet:0.0.1-SNAPSHOT --only-fixed Vulnerability DB [no update available] Loaded image Parsed image Cataloged packages [62 packages] Scanned image [14 vulnerabilities] NAME INSTALLED FIXED-IN TYPE VULNERABILITY SEVERITY jackson-databind 2.13.3 2.13.4 java-archive GHSA-rgv9-q543-rqg4 High jackson-databind 2.13.3 2.13.4.1 java-archive GHSA-jjjh-jjxp-wpff High snakeyaml 1.30 1.31 java-archive GHSA-3mc7-4q67-w48m High snakeyaml 1.30 1.31 java-archive GHSA-98wm-3w3q-mw94 Medium snakeyaml 1.30 1.31 java-archive GHSA-c4r9-r8fh-9vj2 Medium snakeyaml 1.30 1.31 java-archive GHSA-hhhw-99gj-p3c3 Medium snakeyaml 1.30 1.32 java-archive GHSA-9w3m-gqgf-c4p9 Medium snakeyaml 1.30 1.32 java-archive GHSA-w37g-rhq8-7m4j Medium As you can see in the output, only the java-archive vulnerabilities are still present. The other vulnerabilities have been solved. Next, fix the Spring Boot dependency vulnerability. Change the version of Spring Boot from 2.7.0 to 2.7.6 in the POM. XML org.springframework.boot spring-boot-starter-parent 2.7.6 Build the JAR file, build the Docker image, and run the scan again: Shell $ mvn clean verify ... $ mvn dockerfile:build ... $ grype docker:mydeveloperplanet/mygrypeplanet:0.0.1-SNAPSHOT --only-fixed Vulnerability DB [no update available] Loaded image Parsed image Cataloged packages [62 packages] Scanned image [10 vulnerabilities] NAME INSTALLED FIXED-IN TYPE VULNERABILITY SEVERITY snakeyaml 1.30 1.31 java-archive GHSA-3mc7-4q67-w48m High snakeyaml 1.30 1.31 java-archive GHSA-98wm-3w3q-mw94 Medium snakeyaml 1.30 1.31 java-archive GHSA-c4r9-r8fh-9vj2 Medium snakeyaml 1.30 1.31 java-archive GHSA-hhhw-99gj-p3c3 Medium snakeyaml 1.30 1.32 java-archive GHSA-9w3m-gqgf-c4p9 Medium snakeyaml 1.30 1.32 java-archive GHSA-w37g-rhq8-7m4j Medium So, you got rid of the jackson-databind vulnerability, but not of the snakeyaml vulnerability. So, in which dependency is snakeyaml 1.30 being used? You can find out by means of the dependency:tree Maven command. For brevity purposes, only a part of the output is shown here: Shell $ mvnd dependency:tree ... com.mydeveloperplanet:mygrypeplanet:jar:0.0.1-SNAPSHOT [INFO] +- org.springframework.boot:spring-boot-starter-web:jar:2.7.6:compile [INFO] | +- org.springframework.boot:spring-boot-starter:jar:2.7.6:compile [INFO] | | +- org.springframework.boot:spring-boot:jar:2.7.6:compile [INFO] | | +- org.springframework.boot:spring-boot-autoconfigure:jar:2.7.6:compile [INFO] | | +- org.springframework.boot:spring-boot-starter-logging:jar:2.7.6:compile [INFO] | | | +- ch.qos.logback:logback-classic:jar:1.2.11:compile [INFO] | | | | \- ch.qos.logback:logback-core:jar:1.2.11:compile [INFO] | | | +- org.apache.logging.log4j:log4j-to-slf4j:jar:2.17.2:compile [INFO] | | | | \- org.apache.logging.log4j:log4j-api:jar:2.17.2:compile [INFO] | | | \- org.slf4j:jul-to-slf4j:jar:1.7.36:compile [INFO] | | +- jakarta.annotation:jakarta.annotation-api:jar:1.3.5:compile [INFO] | | \- org.yaml:snakeyaml:jar:1.30:compile ... The output shows us that the dependency is part of the spring-boot-starter-web dependency. So, how do you solve this? Strictly speaking, Spring has to solve it. But if you do not want to wait for a solution, you can solve it by yourself. Solution 1: You do not need the dependency. This is the easiest fix and is low risk. Just exclude the dependency from the spring-boot-starter-web dependency in the pom. XML org.springframework.boot spring-boot-starter-web org.yaml snakeyaml Build the JAR file, build the Docker image, and run the scan again: Shell $ mvn clean verify ... $ mvn dockerfile:build ... $ grype docker:mydeveloperplanet/mygrypeplanet:0.0.1-SNAPSHOT --only-fixed Vulnerability DB [no update available] Loaded image Parsed image Cataloged packages [61 packages] Scanned image [3 vulnerabilities] No vulnerabilities found No vulnerabilities are found anymore. Solution 2: You do need the dependency. You can replace this transitive dependency by means of dependencyManagement in the pom. This is a bit more tricky because the updated transitive dependency is not tested with the spring-boot-starter-web dependency. It is a trade-off whether you want to do this or not. Add the following section to the pom: XML org.yaml snakeyaml 1.32 Build the jar file, build the Docker image, and run the scan again: Shell $ mvn clean verify ... $ mvn dockerfile:build ... $ grype docker:mydeveloperplanet/mygrypeplanet:0.0.1-SNAPSHOT --only-fixed Vulnerability DB [no update available] Loaded image Parsed image Cataloged packages [62 packages] Scanned image [3 vulnerabilities] No vulnerabilities found Again, no vulnerabilities are present anymore. Solution 3: This is the solution when you do not want to do anything or whether it is a false positive notification. Create a .grype.yaml file where you exclude the vulnerability with High severity and execute the scan with the --config flag followed by the .grype.yaml file containing the exclusions. The .grype.yaml file looks as follows: YAML ignore: - vulnerability: GHSA-3mc7-4q67-w48m Run the scan again: Shell $ grype docker:mydeveloperplanet/mygrypeplanet:0.0.1-SNAPSHOT --only-fixed Vulnerability DB [no update available] Loaded image Parsed image Cataloged packages [62 packages] Scanned image [10 vulnerabilities] NAME INSTALLED FIXED-IN TYPE VULNERABILITY SEVERITY snakeyaml 1.30 1.31 java-archive GHSA-98wm-3w3q-mw94 Medium snakeyaml 1.30 1.31 java-archive GHSA-c4r9-r8fh-9vj2 Medium snakeyaml 1.30 1.31 java-archive GHSA-hhhw-99gj-p3c3 Medium snakeyaml 1.30 1.32 java-archive GHSA-9w3m-gqgf-c4p9 Medium snakeyaml 1.30 1.32 java-archive GHSA-w37g-rhq8-7m4j Medium The High vulnerability is not shown anymore. 7. Continuous Integration Now you know how to manually scan your Docker images. However, you probably want to scan the images as part of your continuous integration pipeline. In this section, a solution is provided when using Jenkins as a CI platform. The first question to answer is how you will be notified when vulnerabilities are found. Up until now, you only noticed the vulnerabilities by looking at the standard output. This is not a solution for a CI pipeline. You want to get notified and this can be done by failing the build. Grype has the --fail-on flag for this purpose. You probably do not want to fail the pipeline when a vulnerability with severity negligible has been found. Let’s see what happens when you execute this manually. First of all, introduce the vulnerabilities again in the Spring Boot application and in the Docker image. Build the JAR file, build the Docker image and run the scan with flag --fail-on: Shell $ mvn clean verify ... $ mvn dockerfile:build ... $ grype docker:mydeveloperplanet/mygrypeplanet:0.0.1-SNAPSHOT --only-fixed --fail-on high ... 1 error occurred: * discovered vulnerabilities at or above the severity threshold Not all output has been shown here, but only the important part. And, as you can see, at the end of the output, a message is shown that the scan has generated an error. This will cause your Jenkins pipeline to fail and as a consequence, the developers are notified that something went wrong. In order to add this to your Jenkins pipeline, several options exist. Here it is chosen to create the Docker image and execute the grype Docker scan from within Maven. There is no separate Maven plugin for grype, but you can use the exec-maven-plugin for this purpose. Add the following to the build-plugins section of the POM. XML org.codehaus.mojo exec-maven-plugin 3.1.0 grype docker:mydeveloperplanet/mygrypeplanet:${project.version} --scope all-layers --fail-on high --only-fixed -q Two extra flags are added here: --scope all-layers: This will scan all layers involved in the Docker image. -q: This will use quiet logging and will show only the vulnerabilities and possible failures. You can invoke this with the following command: Shell $ mvnd exec:exec You can add this to your Jenkinsfile inside the withMaven wrapper: Plain Text withMaven() { sh 'mvn dockerfile:build dockerfile:push exec:exec' } 8. Conclusion In this blog, you learned how to scan your Docker images by means of grype. Grype has some interesting, user-friendly features which allow you to efficiently add them to your Jenkins pipeline. Also, installing grype is quite easy. Grype is definitely a great improvement over Anchor Engine.
January 24, 2023
by Gunter Rotsaert CORE
· 3,246 Views · 2 Likes
article thumbnail
How Observability Is Redefining Developer Roles
This article will explore observability and inform readers about the evolution of developer roles and how developers can stay ahead of the observability game.
January 24, 2023
by Hiren Dhaduk
· 3,560 Views · 1 Like
article thumbnail
Distributed Stateful Edge Platforms
As companies move to compute and apps closer to where the data is being produced, they need to make their platforms easier and more cost-efficient to manage.
January 24, 2023
by Tom Smith CORE
· 3,025 Views · 1 Like
article thumbnail
5 Factors When Selecting a Database
Here's how to tell when a database is right for your project.
January 24, 2023
by Peter Corless
· 2,679 Views · 5 Likes
article thumbnail
Using QuestDB to Collect Infrastructure Metrics
In this article, readers will learn how QuestDB uses its own database to power the monitoring system of QuestDB Cloud with guide code and helpful visuals.
January 24, 2023
by Steve Sklar
· 2,738 Views · 2 Likes
article thumbnail
Simple Sophisticated Object Cache Service Using Azure Redis
Cache service has been an integral part of major distributed systems. So let's dive deep into how to build a sophisticated object cache service using Redis.
January 23, 2023
by Srivatsan Balakrishnan
· 1,930 Views · 1 Like
article thumbnail
Microservices Discovery With Eureka
In this article, let's explore how to integrate services discovery into a microservices project.
January 22, 2023
by Jennifer Reif CORE
· 4,528 Views · 6 Likes
article thumbnail
What Should You Know About Graph Database’s Scalability?
Graph database scalability how-to, designing a distributed database system, graph database query optimization.
January 20, 2023
by Ricky Sun
· 4,568 Views · 6 Likes
article thumbnail
What Is a Kubernetes CI/CD Pipeline?
A Kubernetes CI/CD pipeline is different from a traditional CI/CD pipeline. The primary difference is the containerization process.
January 20, 2023
by Jyoti Sahoo
· 4,475 Views · 3 Likes
article thumbnail
Understanding gRPC Concepts, Use Cases, and Best Practices
In this article, we are going to understand what RPC is, and the various implementations of RPC, with a focus on gRPC, which is Google's implementation of RPC.
January 20, 2023
by Hitesh Pattanayak
· 4,720 Views · 2 Likes
article thumbnail
How To Create a Stub in 5 Minutes
Readers will learn how to create stubs in five minutes, which uses regression and load testing, debugging, and more, and how to configure the stubs flexibly.
January 17, 2023
by Andrei Rogalenko
· 2,987 Views · 3 Likes
article thumbnail
A Cloud-Native SCADA System for Industrial IoT Built With Apache Kafka
Building a next-generation cloud-native SCADA infrastructure with Apache Kafka for secure Industrial IoT across the edge and public cloud.
January 17, 2023
by Kai Wähner CORE
· 2,789 Views · 2 Likes
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • ...
  • Next

ABOUT US

  • About DZone
  • Send feedback
  • Careers
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 600 Park Offices Drive
  • Suite 300
  • Durham, NC 27709
  • support@dzone.com
  • +1 (919) 678-0300

Let's be friends: