Introduction to Microservices
Microservices architecture has gained immense popularity in recent years as a scalable and efficient approach to developing complex software systems. In this section, we will explore the concept of microservices and discuss their advantages.
What are Microservices?
Microservices, also known as the microservices architectural style, is an architectural approach that structures an application as a collection of small, autonomous services that are independently deployable. Each service focuses on a specific business capability and can be developed, deployed, and managed independently.
Unlike traditional monolithic applications, where all functionality is packaged into a single unit, microservices divide the application into multiple small services. Each service is responsible for a distinct set of tasks and communicates with other services through well-defined APIs, typically using lightweight protocols such as HTTP or messaging queues.
Advantages of Microservices
Microservices offer several advantages over monolithic architectures:
Modularity and Scalability: The modular nature of microservices allows for independent development and deployment of each service. This enables teams to work on different services concurrently and scale individual services based on demand.
Flexibility and Agility: Microservices promote flexibility and agility by decoupling the application into smaller components. This allows teams to develop, test, and deploy services independently, making it easier to introduce new features or make changes without impacting the entire system.
Technology Diversity: With microservices, different services can be developed using different technologies and programming languages. This enables teams to choose the most suitable technology stack for each service, based on its specific requirements.
Fault Isolation and Resilience: Microservices architecture provides fault isolation, meaning the failure of one service does not affect the entire system. Services can implement individual error handling and recovery mechanisms, improving the overall resilience of the application.
Improved Scalability and Performance: Microservices allow granular scalability, where only the required services can be scaled independently. This approach improves overall system performance, as resources can be allocated more optimally.
In the upcoming lessons, we will delve deeper into each of these advantages and explore how to design, develop, and deploy microservices using Java and Spring Boot.
Let's test your knowledge. Is this statement true or false?
Microservices architecture structures an application as a collection of small, autonomous services that are independently deployable.
Press true if you believe the statement is correct, or false otherwise.
Creating a Spring Boot Project
To build microservices with Spring Boot, we first need to set up a new Spring Boot project and configure the necessary dependencies. In this section, we will walk through the steps to create a Spring Boot project.
Step 1: Prerequisites
Before we begin, make sure you have the following prerequisites:
- Java Development Kit (JDK) installed
- Spring Boot CLI installed
Step 2: Create a new Spring Boot project
To create a new Spring Boot project, open your terminal and run the following command:
1$ spring init --name=myproject --groupId=com.example --dependencies=web myproject
In the command above, replace myproject
with the desired name for your project. The --groupId
option specifies the project's group ID and --dependencies
option specifies the required dependencies. In this example, we are using the web
dependency to include the Spring Web module.
Step 3: Configure project dependencies
Next, navigate into the project folder using the cd
command:
1$ cd myproject
Open the pom.xml
file in a text editor and add any additional dependencies your project requires. For example, if you need to include the Spring Data JPA module, add the following code snippet inside the <dependencies>
tag:
1<dependency>
2 <groupId>org.springframework.boot</groupId>
3 <artifactId>spring-boot-starter-data-jpa</artifactId>
4</dependency>
Save the pom.xml
file after making the necessary changes.
Step 4: Build and run the project
To build the project, run the following command:
1$ mvn clean install
Once the project is successfully built, you can run it using the following command:
1$ java -jar target/myproject.jar
Congratulations! You have successfully created a Spring Boot project. In the upcoming lessons, we will explore how to build microservices and implement various features using Spring Boot.
Build your intuition. Click the correct answer from the options.
Which of the following is not a step in creating a Spring Boot project?
Click the option that best answers the question.
- Installing the Java Development Kit (JDK)
- Configuring project dependencies
- Building and running the project
- Setting up a virtual environment
Building a Simple Microservice
In this section, we will focus on building a simple microservice using Spring Boot. Our microservice will have a REST API endpoint that returns a simple "Hello, World!" message.
To get started, we need to create a new Java class that will serve as our controller. The controller will define the routes and handle the incoming HTTP requests. Let's create a new class called HelloController
:
1${code}
In the code snippet above, we have defined a HelloController
class with a sayHello
method. This method is annotated with @GetMapping("/hello")
, which specifies that it should handle the GET requests to the "/hello" route. Inside the method, we simply return the string "Hello, World!".
Once we have the controller in place, we need to configure our microservice to run. We can do this by creating a main
method in a separate class, such as Application
, and starting a Spring Boot application context:
1public class Application {
2 public static void main(String[] args) {
3 SpringApplication.run(Application.class, args);
4 }
5}
In the main
method above, we are using SpringApplication.run
to start the Spring Boot application. We pass Application.class
as the first argument, which indicates the primary configuration class for our application.
That's it! We have now created a simple microservice with a REST API endpoint. Next, we will learn how to test our microservice and make HTTP requests to it.
xxxxxxxxxx
class HelloController {
"/hello") (
public String sayHello() {
return "Hello, World!";
}
}
Let's test your knowledge. Is this statement true or false?
Breadth-first search is a graph traversal algorithm that visits all of the direct neighbors of a node before visiting any of its descendants.
Press true if you believe the statement is correct, or false otherwise.
Implementing Service Discovery
In a microservices architecture, service discovery plays a vital role in enabling dynamic service registration and discovery. It allows services to locate and communicate with each other without hardcoding their network locations. In this section, we will learn how to implement service discovery in a Java microservices application using Spring Boot and Eureka.
To get started, we need to add the Eureka client dependency to our project. Open your pom.xml
file and add the following dependency:
1<dependency>
2 <groupId>org.springframework.cloud</groupId>
3 <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
4</dependency>
In the code snippet above, we are adding the spring-cloud-starter-netflix-eureka-client
dependency, which provides the necessary libraries for integrating with Eureka.
Next, we need to configure our microservice to register with the Eureka server. We can do this by adding the following configuration to our application.properties
file:
1spring.application.name=my-microservice
2spring.cloud.discovery.client.simple.instances.eureka[0].hostname=localhost
3spring.cloud.discovery.client.simple.instances.eureka[0].port=8080
In the configuration above, we are setting the spring.application.name
property to the name of our microservice and configuring the Eureka server location.
Finally, we need to annotate our main class with @EnableDiscoveryClient
to enable service discovery:
1import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
2
3@EnableDiscoveryClient
4public class Application {
5 public static void main(String[] args) {
6 SpringApplication.run(Application.class, args);
7 }
8}
In the code snippet above, we are using @EnableDiscoveryClient
to enable service discovery for our microservice.
With the above setup, our microservice will now register with the Eureka server upon startup. Other microservices can discover and communicate with our microservice using its registered name. We can easily retrieve the registered instances of a microservice using the DiscoveryClient
class provided by Spring Cloud. For example:
1import org.springframework.cloud.client.discovery.DiscoveryClient;
2
3@Autowired
4private DiscoveryClient discoveryClient;
5
6public void getServiceInstances() {
7 List<ServiceInstance> instances = discoveryClient.getInstances("my-microservice");
8 // Process the instances as needed
9}
In the code snippet above, we are autowiring the DiscoveryClient
and using it to retrieve the registered instances of our microservice with the name "my-microservice".
That's it! We have now implemented service discovery in our Java microservices application using Spring Boot and Eureka. Service discovery allows our microservices to find and communicate with each other dynamically, making our architecture more flexible and scalable.
xxxxxxxxxx
const player = "Kobe Bryant";
Are you sure you're getting this? Click the correct answer from the options.
What is the purpose of service discovery in a microservices architecture?
Click the option that best answers the question.
- To enable dynamic service registration and discovery
- To secure microservices and restrict access
- To handle fault tolerance and resilience
- To scale microservices and load balance between instances
Communicating Between Microservices
Implementing communication between microservices is essential for building a robust and scalable microservices architecture. In this section, we will explore two common mechanisms for inter-service communication: REST APIs and message queues.
Using REST APIs
In a microservices architecture, communication between microservices is typically done through REST APIs. REST (Representational State Transfer) is an architectural style for designing networked applications. It provides a standard set of constraints that promote scalability, reliability, and simplicity. With REST, microservices can exchange data and trigger actions by making HTTP requests to each other's APIs.
For example, let's consider a scenario where we have two microservices: User Service and Order Service. The User Service is responsible for managing user information, while the Order Service handles order processing. When a user places an order, the Order Service needs to retrieve user details from the User Service. This can be achieved by making a REST API call from the Order Service to the User Service. The Order Service can make an HTTP GET request to the User Service's API endpoint to retrieve the user information.
Here's an example of how the Order Service can make a REST API call to the User Service using the Spring RestTemplate:
1import org.springframework.http.HttpHeaders;
2import org.springframework.http.HttpMethod;
3import org.springframework.http.ResponseEntity;
4import org.springframework.web.client.RestTemplate;
5
6RestTemplate restTemplate = new RestTemplate();
7HttpHeaders headers = new HttpHeaders();
8// Set any required headers
9
10ResponseEntity<User> response = restTemplate.exchange(
11 "http://user-service/api/users/{userId}",
12 HttpMethod.GET,
13 null,
14 User.class,
15 userId
16);
17
18User user = response.getBody();
19
20// Process the user data
In the code snippet above, we are creating a RestTemplate instance and setting any required headers. Then, we use the exchange
method to make a GET request to the User Service's API endpoint. We pass the URL, HTTP method, request entity (null in this case), response type (User.class), and any path variables (userId in this case). Finally, we can extract the user data from the response and process it as needed.
Using Message Queues
Another communication mechanism commonly used in microservices architecture is message queues. Message queues provide asynchronous and decoupled communication between microservices, making them a robust and scalable solution.
With message queues, a microservice sends messages to a queue, and other microservices can consume those messages from the queue at their own pace. This enables loose coupling and allows microservices to scale independently, as they don't need to wait for other microservices to process the data.
Popular message queue implementations include RabbitMQ, Apache Kafka, and AWS SQS. In Spring Boot, we can easily integrate with these message queues using the respective Spring Boot starters.
To use a message queue, we typically define a message producer in the sending microservice and a message consumer in the receiving microservice. The producer sends messages to the queue, while the consumer listens for messages and processes them.
Here's an example of using Spring Cloud Stream with RabbitMQ as the message queue:
1import org.springframework.cloud.stream.annotation.EnableBinding;
2import org.springframework.cloud.stream.annotation.StreamListener;
3import org.springframework.cloud.stream.messaging.Processor;
4import org.springframework.messaging.handler.annotation.Payload;
5
6@EnableBinding(Processor.class)
7public class MessageConsumer {
8 @StreamListener(Processor.INPUT)
9 public void consumeMessage(@Payload Message message) {
10 // Process the message
11 }
12}
In the code snippet above, we enable Spring Cloud Stream by annotating the class with @EnableBinding
and specifying the input channel using Processor.INPUT
. Then, we define a method annotated with @StreamListener
to consume the messages from the input channel. The method takes a Message
parameter, which represents the message received from the queue. We can then process the message as needed.
Conclusion
Communication between microservices is a crucial aspect of a microservices architecture. Using REST APIs and message queues, microservices can exchange data and trigger actions in a scalable and decoupled manner. With Spring Boot, we have powerful tools and libraries available to implement these communication mechanisms easily.
xxxxxxxxxx
// <dependency>
// <groupId>org.springframework.boot</groupId>
// <artifactId>spring-boot-starter-web</artifactId>
// </dependency>
//
Are you sure you're getting this? Click the correct answer from the options.
Which communication mechanism is commonly used in microservices architecture to achieve loose coupling and enable independent scalability?
Click the option that best answers the question.
- REST APIs
- SOAP APIs
- GraphQL
- gRPC
Securing Microservices
Securing microservices is a critical aspect of building a robust and reliable microservices architecture. By adding security measures, we can protect our microservices from unauthorized access, data breaches, and other security threats. In this section, we will explore various security practices and techniques to secure our microservices.
Authentication and Authorization
Authentication and authorization are the fundamental building blocks of microservices security. Authentication ensures that the request sender is a legitimate user or service, while authorization controls what actions that user or service can perform. In a microservices architecture, we typically implement authentication and authorization at multiple levels, including the API gateway, individual microservices, and external service integrations.
When it comes to implementing authentication, Spring Security provides excellent support. It offers a comprehensive set of features to handle user authentication, including support for various authentication providers, such as OAuth2, JWT, and LDAP. By integrating Spring Security with our microservices, we can ensure that only authenticated users or services can access the protected resources.
For example, let's consider a microservice that exposes a REST API for managing user profiles. To secure this microservice, we can apply authentication and authorization techniques using Spring Security. We can configure the microservice to validate JWT tokens, which are issued by an external authorization server. Only requests with valid tokens will be allowed to access the protected resources.
Here's an example of how to configure Spring Security in a Spring Boot microservice:
1@EnableWebSecurity
2public class SecurityConfig extends WebSecurityConfigurerAdapter {
3
4 @Override
5 protected void configure(HttpSecurity http) throws Exception {
6 http
7 .authorizeRequests()
8 .antMatchers("/api/users/**").hasRole("ADMIN")
9 .anyRequest().authenticated()
10 .and()
11 .oauth2ResourceServer()
12 .jwt();
13 }
14
15}
In the code snippet above, we enable Spring Security by annotating the configuration class with @EnableWebSecurity
. We then override the configure
method to define our security rules. In this example, we allow only requests to the /api/users/**
endpoint with the ADMIN
role and ensure that any authenticated request can access other endpoints. Finally, we configure the microservice to use JWT tokens for authentication using the oauth2ResourceServer().jwt()
method.
Data Protection
Securing sensitive data within microservices is another crucial consideration. Microservices often handle sensitive and confidential information, such as user credentials, personal data, and financial records. It's essential to protect this data from unauthorized access or leakage.
One effective approach to data protection is to apply encryption techniques. We can encrypt the data at rest and in transit to ensure that it remains confidential. For example, we can use HTTPS (HTTP over SSL/TLS) to secure data transmission between microservices. Additionally, we can encrypt sensitive data stored in databases or caches using encryption algorithms, such as AES (Advanced Encryption Standard).
Another important aspect of data protection is implementing data masking or anonymization techniques. By masking sensitive data in logs, error messages, or response payloads, we can prevent accidental exposure of sensitive information. Data anonymization can be achieved by replacing personally identifiable information (PII) with anonymous or pseudonymous values, thus preserving privacy while still allowing data processing.
Security Testing
Regular security testing and vulnerability assessments are crucial to ensure that our microservices are secure. By performing security testing, including penetration testing and vulnerability scanning, we can identify and address potential security vulnerabilities before they are exploited by attackers. We should also conduct regular security audits and code reviews to ensure that security best practices are followed throughout the development and deployment processes.
In addition to external security testing, we should also implement automated security checks within our microservices. This can include implementing secure coding practices, such as input validation, output encoding, and proper error handling. Properly configuring and monitoring logs and audit trails can also help in detecting and responding to security incidents.
Conclusion
Securing microservices is a critical aspect of building a reliable and robust microservices architecture. By implementing authentication and authorization, data protection techniques, and regular security testing, we can ensure that our microservices are protected against security threats. Spring Security provides powerful tools and features that help in securing microservices and integrating with popular authentication and authorization protocols and mechanisms.
Let's test your knowledge. Is this statement true or false?
Spring Security is a comprehensive security framework that provides features for authentication and authorization in microservices.
Press true if you believe the statement is correct, or false otherwise.
Handling Fault Tolerance and Resilience
In a microservices architecture, handling failures and ensuring resilience is of utmost importance. Microservices are distributed systems, and failure in any one microservice can potentially affect the entire system. Thus, it is crucial to implement mechanisms to handle failures and ensure the overall resilience of the microservices.
One common approach to handle fault tolerance and resilience is through the use of circuit breakers. A circuit breaker is a design pattern that can detect and handle failures in microservices. It acts as a safeguard by providing a fallback mechanism when a microservice fails or becomes unresponsive.
When a microservice encounters an error or becomes overloaded, the circuit breaker can open, preventing further requests from being sent to the failing microservice. Instead, it can return a cached response or a default value. This helps to prevent cascading failures and allows the system to gracefully degrade when one or more microservices are unavailable.
To implement a circuit breaker, we can use libraries such as Hystrix or Resilience4j. These libraries provide mechanisms to declaratively define circuit breakers, configure timeouts and retries, and specify fallback behaviors. Let's take a look at an example of implementing a circuit breaker using Hystrix in a Spring Boot application:
1@RestController
2public class ExampleController {
3
4 @GetMapping("/example")
5 @HystrixCommand(fallbackMethod = "fallbackMethod")
6 public String exampleEndpoint() {
7 // Call the microservice API
8 return microserviceClient.doSomething();
9 }
10
11 public String fallbackMethod() {
12 // Fallback logic
13 return "Fallback response";
14 }
15
16}
xxxxxxxxxx
class Main {
public static void main(String[] args) {
// Implement fault tolerance and resilience logic here
}
}
Are you sure you're getting this? Fill in the missing part by typing it in.
A __ provides a fallback mechanism when a microservice fails or becomes unresponsive.
Write the missing line below.
Scaling and Load Balancing
In a microservices architecture, scaling and load balancing are crucial for ensuring the availability and performance of the system. As the traffic to the microservices increases, it's necessary to scale the application horizontally by adding more instances of microservices.
Scaling allows the system to handle a higher volume of requests by distributing the load across multiple instances of the microservice. This helps to avoid overloading a single instance and improves the overall responsiveness and reliability of the system.
To achieve scaling in a microservices environment, load balancing is implemented. Load balancing ensures that the incoming requests are evenly distributed among the available instances of the microservice. This distribution optimizes the resource utilization and prevents any single instance from becoming a bottleneck.
There are several load balancing algorithms that can be used, such as Round Robin, Weighted Round Robin, Least Connection, and Random. These algorithms determine how the requests are distributed among the microservice instances based on factors like the current load, response times, or defined weights.
Here's an example code snippet that demonstrates a basic load balancing algorithm:
1import java.util.List;
2import java.util.ArrayList;
3
4class Main {
5 public static void main(String[] args) {
6 // Simulate multiple instances of microservices
7 List<Microservice> microservices = new ArrayList<>();
8 for (int i = 0; i < 10; i++) {
9 microservices.add(new Microservice());
10 }
11
12 // Calculate the total load on all microservices
13 int totalLoad = microservices.stream()
14 .mapToInt(Microservice::getLoad)
15 .sum();
16
17 // Determine the average load of each microservice
18 int averageLoad = totalLoad / microservices.size();
19
20 // Implement load balancing algorithm to distribute load evenly
21 for (Microservice microservice : microservices) {
22 int excessLoad = microservice.getLoad() - averageLoad;
23 if (excessLoad > 0) {
24 // Balance excess load to other microservices
25 microservice.sendLoadToOtherMicroservices(microservices, excessLoad);
26 }
27 }
28
29 // Verify load distribution after load balancing
30 int newTotalLoad = microservices.stream()
31 .mapToInt(Microservice::getLoad)
32 .sum();
33 int newAverageLoad = newTotalLoad / microservices.size();
34
35 // Output the load distribution
36 System.out.println("Load distribution after load balancing:");
37 for (Microservice microservice : microservices) {
38 System.out.println("Microservice " + microservice.getId() + ": Load = " + microservice.getLoad());
39 }
40 }
41}
42
43class Microservice {
44 private static int counter = 0;
45 private int id;
46 private int load;
47
48 public Microservice() {
49 this.id = counter++;
50 this.load = (int) (Math.random() * 100) + 1;
51 }
52
53 public int getId() {
54 return id;
55 }
56
57 public int getLoad() {
58 return load;
59 }
60
61 public void sendLoadToOtherMicroservices(List<Microservice> microservices, int excessLoad) {
62 int remainingLoad = excessLoad;
63 for (Microservice microservice : microservices) {
64 if (microservice != this && microservice.getLoad() < excessLoad - remainingLoad) {
65 int transferLoad = Math.min(microservice.getLoad(), remainingLoad);
66 microservice.receiveLoad(transferLoad);
67 this.load -= transferLoad;
68 remainingLoad -= transferLoad;
69 }
70 }
71 }
72
73 public void receiveLoad(int load) {
74 this.load += load;
75 }
76}
In this example, we simulate multiple instances of microservices and calculate the total load on all microservices. We determine the average load and then distribute the excess load to other microservices to achieve load balancing. Finally, we output the load distribution after load balancing.
Load balancing plays a vital role in ensuring that the microservices can handle increased traffic efficiently and ensures the overall performance and availability of the system.
xxxxxxxxxx
}
import java.util.List;
import java.util.ArrayList;
class Main {
public static void main(String[] args) {
// Simulate multiple instances of microservices
List<Microservice> microservices = new ArrayList<>();
for (int i = 0; i < 10; i++) {
microservices.add(new Microservice());
}
// Calculate the total load on all microservices
int totalLoad = microservices.stream()
.mapToInt(Microservice::getLoad)
.sum();
// Determine the average load of each microservice
int averageLoad = totalLoad / microservices.size();
// Implement load balancing algorithm to distribute load evenly
for (Microservice microservice : microservices) {
int excessLoad = microservice.getLoad() - averageLoad;
if (excessLoad > 0) {
// Balance excess load to other microservices
microservice.sendLoadToOtherMicroservices(microservices, excessLoad);
}
}
// Verify load distribution after load balancing
Let's test your knowledge. Fill in the missing part by typing it in.
Load balancing ensures that the incoming requests are evenly distributed among the available instances of the microservice. This distribution optimizes the ____ utilization and prevents any single instance from becoming a bottleneck.
Write the missing line below.
Deploying Microservices to the Cloud
When it comes to deploying microservices, cloud platforms provide a scalable and reliable environment. Platforms like AWS (Amazon Web Services), Azure, and Google Cloud offer various services and tools that make it easier to deploy, manage, and scale microservices.
There are several ways to deploy microservices to the cloud, depending on the specific platform and requirements of the application. Some common deployment options include:
- Virtual Machines (VMs): In this approach, each microservice runs on a separate VM. It provides isolation and flexibility but requires managing the infrastructure and scaling.
- Containerization: Using containerization technologies like Docker allows packaging microservices along with their dependencies into containers. Containers offer lightweight and consistent environments that can be easily deployed and managed on cloud platforms.
- Serverless Computing: With serverless platforms such as AWS Lambda and Azure Functions, you can deploy microservices as individual functions. The platform takes care of scaling, resource allocation, and infrastructure management.
The choice of deployment option depends on factors such as scalability requirements, resource utilization, operational flexibility, and familiarity with the platform.
Let's take a look at an example of deploying a simple microservice to AWS Lambda using Java and the AWS Serverless Application Model (SAM).
1import com.amazonaws.services.lambda.runtime.Context;
2import com.amazonaws.services.lambda.runtime.RequestHandler;
3
4public class GreetingHandler implements RequestHandler<Request, Response> {
5
6 public Response handleRequest(Request request, Context context) {
7 String name = request.getName();
8 String greeting = "Hello, " + name + "!";
9 return new Response(greeting);
10 }
11
12 public static class Request {
13 private String name;
14
15 public String getName() {
16 return name;
17 }
18
19 public void setName(String name) {
20 this.name = name;
21 }
22 }
23
24 public static class Response {
25 private String message;
26
27 public Response(String message) {
28 this.message = message;
29 }
30
31 public String getMessage() {
32 return message;
33 }
34
35 public void setMessage(String message) {
36 this.message = message;
37 }
38 }
39
40}
In this example, we have a simple microservice implemented as an AWS Lambda function. It takes a name
as input and returns a greeting message. The GreetingHandler
class handles the Lambda function invocation and performs the necessary logic.
By deploying this microservice to AWS Lambda, we can take advantage of the serverless architecture, which automatically handles scaling and resource allocation based on the incoming requests. This allows for efficient resource utilization and cost optimization.
When deploying microservices to the cloud, it's important to consider factors such as security, monitoring, and scalability. Cloud platforms offer various services and tools for managing and securing microservices, such as AWS CloudWatch for monitoring, Azure Key Vault for managing secrets, and Google Cloud Load Balancer for distributing traffic.
As a senior engineer experienced in Java, Spring, Spring Boot, and AWS, you already have knowledge and skills that will greatly contribute to the successful deployment of microservices to the cloud. Keep learning and experimenting with cloud platforms and their services to enhance your understanding and expertise in deploying microservices in a cloud-native environment.
xxxxxxxxxx
class Main {
public static void main(String[] args) {
// replace with your Java logic here
for(int i = 1; i <= 100; i++) {
if(i % 3 == 0 && i % 5 == 0) {
System.out.println("FizzBuzz");
} else if(i % 3 == 0) {
System.out.println("Fizz");
} else if(i % 5 == 0) {
System.out.println("Buzz");
} else {
System.out.println(i);
}
}
}
}
Build your intuition. Click the correct answer from the options.
Which of the following is NOT a common deployment option for microservices on cloud platforms?
Click the option that best answers the question.
- Virtual Machines (VMs)
- Containerization
- Serverless Computing
- Physical Servers
Generating complete for this lesson!