Serverless vs Containers: Choosing the Right Architecture for Your Application
Compare serverless vs. container-based architectures for cost, performance, and scalability. Learn key differences and choose the best fit for your app.
Join the DZone community and get the full member experience.
Join For FreeChoosing the right architecture for your application is crucial to make it low-cost, performant, and scalable. Two of the leading software development methods today, serverless and container-based architectures, have distinct patterns for application release and application processing. In this article, we discuss their technical intricacies, key distinctions, and under which conditions to make use of each, with code examples to illustrate specific application uses.
What Is Serverless Architecture?
Serverless computing eliminates infrastructure administration, leaving developers to write code alone. Provisioning, scaling, and servicing are controlled by cloud platforms such as AWS Lambda, Azure Functions, and Google Cloud Functions.
How Serverless Works
Developers design tiny single-purpose functions to execute on a per-event basis. They're called by HTTP requests, by database events, or by queue messages. You're billed by the provider to execute functions only as needed. Let's see an example implementation in Python:
import json
def lambda_handler(event, context):
# Extract data from the incoming request
name = event.get("name", "World")
# Construct a response
return {
"statusCode": 200,
"body": json.dumps({
"message": f"Hello, {name}!"
})
}
Here, the Lambda function executes exclusively at the time it is called, and the user is billed based on the execution time.
What Is a Container-Based Architecture?
Containerization bundles an application and all of its dependencies into a light, portable package that runs reliably across different environments. Docker and orchestration tools, such as Kubernetes, form the basis for this practice.
How Containers Work
Containers bundle application, runtime, libraries, and system tools into one package, ensuring consistency across development, test, and production environments. Here's an example of a Dockerfile
(Instructions on how to build an image used for a container):
# Use the official Node.js image as the base
FROM node:16
# Set the working directory inside the container
WORKDIR /app
# Copy application files to the container
COPY package.json package-lock.json ./
RUN npm install
# Copy the remaining app files
COPY . .
# Expose the application port
EXPOSE 3000
# Start the app
CMD ["npm", "start"]
Here, the application and the dependencies are bundled into one package that can be used to run on any Docker-supported environment.
Key Technical Differences
Serverless and containers handle infrastructure, scalability, state, performance, and costs in a dramatically different manner. Infrastructure is fully abstracted and handled by the cloud provider in serverless, and developers are not required to provision or monitor servers. Containers grant more autonomy to developers when specifying the runtime environment and having to handle orchestration tools such as Kubernetes or Docker Swarm to deploy and scale the applications.
For scalability, the function is automatically and instantaneously scaled based on the requests coming in, ideal for spiky, variable, and unpredictable workloads. Containers can be scaled, but they need to be configured using automated scaling mechanisms and metrics in an explicit manner. That adds operational overhead but offers more control.
Another key divergence is state management. Serverless functions are stateless, which means any data to be kept across calls needs to be stored in databases or external services. Containers can store state in memory or on disk while they run, which is adequate for certain loads but complicates distributed systems and scaling.
On the performance front for start-ups, serverless applications can suffer from "cold start" delays, which affect latency-sensitive applications, when functions are called after an idle period. Containers, especially long-running or preload containers, boot more quickly and reliably, but consume more resources to stay alive.
Finally, there are multiple billing models: serverless provides the pay-per-use model, where the customers are billed based on invocations and the time their code is executing. Containers are billed based on the provisioned compute (CPU, memory), regardless of whether they are utilizing it. Serverless is more appealing to intermittent workloads, while containers can be economical if the application is resource-intensive and long-running.
Comparison
aspect | serverless | containers |
---|---|---|
Infrastructure |
Fully abstracted and managed by the cloud provider. |
Developer-defined and requires orchestration. |
Scalability |
Auto-scales based on function invocations. |
Requires configuration, e.g., Kubernetes auto-scaling. |
State Management |
Stateless by default, external storage is required. |
Can maintain state within containers if needed. |
Startup Time |
Cold starts can cause latency. |
Starts quickly but requires pre-provisioned resources. |
Billing |
Pay-per-invocation and execution time. |
Pay for resources (CPU, memory) regardless of usage. |
When to Use Serverless
Serverless is used primarily for:
- Event-driven applications: Most appropriate for applications like real-time data processing, where functions are invoked based on events.
- API backends: Simplify the process of developing APIs using tools like AWS Lambda and API Gateway.
- Low-traffic apps: Reduce costs for low-usage applications.
An example of serverless API deployment using AWS SAM:
Resources:
MyFunction:
Type: AWS::Serverless::Function
Properties:
Handler: app.lambda_handler
Runtime: python3.9
Events:
ApiEvent:
Type: Api
Properties:
Path: /hello
Method: get
When to Use Containers
Containers are used primarily for:
- Microservices: Run many independently deployable services.
- Stateful applications: Databases, application caching, and session persistence applications.
- Custom environments: Apps with complex dependencies or specific runtime requirements.
An example of Node.js application deployment using Kubernetes:
apiVersion: apps/v1
kind: Deployment
metadata:
name: nodejs-app
spec:
replicas: 3
selector:
matchLabels:
app: nodejs-app
template:
metadata:
labels:
app: nodejs-app
spec:
containers:
- name: nodejs-app
image: my-nodejs-app:latest
ports:
- containerPort: 3000
Performance Considerations
If you are comparing serverless and container architecture based on their performance factors, you should be aware of how both systems handle latency, concurrency, and resource usage.
Serverless functions might suffer from cold start latency, where the function is slower than usual to initialize after having remained idle for some time. That can create perceivable latency, especially for user-facing applications or APIs, which should be able to exhibit low-predictable latency. Cloud provider infrastructures typically have concurrency caps (AWS Lambda, for instance, has the policy to limit concurrent invocations by default) that can affect throughput and responsiveness in the midst of bursts unless moved up or otherwise.
Containers, then, offer finer-grained, predictable resource control. Developers can allocate an explicit amount of memory and CPU, with finer isolation and tuning available to high-demanding applications. Containers offer memory persistence for state, which can be used to cache information and prevent redundant processing, which is a capability that is missing in stateless serverless systems. Containers are therefore used for high-throughput applications that require predictable performance, session state handling, cache layers, or data pipes.
Serverless, overall, makes things easier to execute at the expense of latency and scaling issues under specific loads, while containers yield more predictable and controllable performance for the price of more to be managed.
Serverless
- Cold starts: Latency can be caused by an extended idle time of an underlying function
- Concurrency limitations: Concurrency in cloud providers is restricted.
Containers
- Resource management: Containers provide finer-grained resource usage control (CPU/memory).
- Persistent state: Containers can store in-memory data to improve their efficiency.
Deployment and CI/CD
Serverless CI/CD Pipeline
Serverless deployment through a CI/CD pipeline is all about releasing individual functions, and not the complete applications or services. Developers keep their function code in repositories like GitHub or AWS CodeCommit, and automated build and release are carried out by tools such as AWS CodePipeline.
Building and testing functions on any commit change is carried out by AWS CodeBuild, and packaging and releasing code, along with necessary infrastructure configurations, are accomplished through frameworks such as AWS SAM (Serverless Application Model) or the Serverless Framework. A lightweight, events-driven release with low operating overhead is most suited to agile groups leveraging rapid iteration and scalable design without having to directly handle servers.
By leveraging automated deployments using AWS CodePipeline, you can:
- Create Functions within a repository
- Deploy to AWS SAM or Serverless Framework.
Here's an example of a pipeline definition (AWS CodePipeline):
{
"stages": [
{
"name": "Source",
"actions": [
{
"name": "Source",
"actionTypeId": { "category": "Source", "provider": "GitHub" },
"configuration": { "Branch": "main" }
}
]
},
{
"name": "Deploy",
"actions": [
{
"name": "DeployFunction",
"actionTypeId": { "category": "Deploy", "provider": "Lambda" },
"configuration": { "FunctionName": "MyLambdaFunction" }
}
]
}
]
}
Container-Based CI/CD
Container-Based CI/CD relies upon Docker and Kubernetes for application, testing, and deployment of software. According to this practice, code is bundled into Docker images, which give you standardized runtime environments across the software life cycle. Each one is then pushed into a Docker registry such as Docker Hub or GitHub Container Registry.
Kubernetes is then used for scaling the deployment of images using declarative YAML manifests specifying the application state that needs to be reached, for example, resource quotas, ingress port numbers, and the number of replicas for containers. For automating this process, one can utilize GitHub Actions to trigger Docker builds and pushes upon code check-in, simplifying the creation of an automated CI pipeline.
Beyond avoiding manual mistakes and configuration drift, this configuration can handle rapid iteration, parallel deployments, as well as scalable infrastructure administration. This makes it a prominent DevOps and cloud-native application model.
Using Docker and Kubernetes, you can:
- Build a Docker image and push to a container registry (e.g., Docker Hub)
- Deploy with a Kubernetes YAML manifest.
Here's an example YAML config:
name: Docker Build and Push
on:
push:
branches:
- main
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Build Docker Image
run: docker build -t my-app:latest .
- name: Push to Docker Hub
run: docker push my-app:latest
Security Implications
Security is one of the primary concerns in deciding between serverless and containerized architectures since they both present their own risks and mitigations. Serverless provides robust isolation through executing functions within ephemeral, event-driven environments that reduce attack surface and exposure. Without permanent servers or open ports, classic channels like port scanning are pretty much eliminated. However, as developers, we must manage permissions, secure APIs, and audit dependencies.
Containers, while offering more flexibility, come along with more security concerns. Misconfigurations such as open ports or high-level privileges can leave systems vulnerable to attack. Outdated or untrusted base images causing image vulnerabilities are another major threat. Because containers typically share the same kernel they're running on, one compromised container can attack others if it is not properly isolated. Containers, therefore, must be image-scanned, subjected to severe role-based access control, and guarded against runtime attacks.
Serverless
- Isolating functions: Functions are isolated on the call level
- Limited attack surface: Low exposure based on specific events that trigger functions.
Containers
- Increased exposure: Misconfigured containers can make entire environments vulnerable
- Image vulnerabilities: Base images can be insecure or out-of-step.
Conclusion
Serverless and container-based architectures fulfill distinct purposes, and their applicability varies based on application needs. Choose serverless for very dynamic, event-driven, or low-traffic applications that don't require frequent maintenance. Use containers for applications that need more control, customized environments, or durable state. By considering the technical compromises, developers can achieve optimization in efficiency and scalability in their software using the right tools.
Opinions expressed by DZone contributors are their own.
Comments