Batch processing is a cornerstone of modern data pipelines, machine learning inference, and backend operations. From processing daily reports to crunching large datasets, these workloads are often characterized by their episodic nature: they run, complete their task, and then terminate. While Kubernetes excels at managing long-running services, effectively scaling batch jobs to meet fluctuating demand while optimizing resource consumption has historically been a challenge. Traditional Kubernetes Jobs are great for single-shot tasks, but they lack inherent autoscaling capabilities based on external metrics.
Enter KEDA (Kubernetes Event-Driven Autoscaling). KEDA extends Kubernetes to allow event-driven scaling for any container, including Deployments, StatefulSets, and crucially, Jobs. For batch workloads, KEDA introduces the concept of ScaledJob, which dynamically creates and manages Kubernetes Jobs based on the length of a queue, the number of messages in a stream, or various other custom metrics. This means your batch processing infrastructure can scale out instantly when a surge of work arrives and scale back to zero when the queue is empty, leading to significant cost savings and improved efficiency.
This comprehensive guide will walk you through setting up KEDA and leveraging ScaledJobs to intelligently autoscale your batch workloads. We’ll cover everything from installation and configuration to practical examples and best practices, ensuring your Kubernetes clusters are humming with efficient, event-driven batch processing. Get ready to transform how you manage episodic tasks in Kubernetes!
TL;DR: Autoscaling Batch Jobs with KEDA ScaledJobs
KEDA’s ScaledJob resource enables event-driven autoscaling for Kubernetes batch workloads, dynamically creating Jobs based on external metrics like queue length. This optimizes resource usage and costs.
- Install KEDA:
helm repo add kedacore https://kedacore.github.io/charts && helm install keda kedacore/keda --namespace keda --create-namespace - Define a ScaledJob: Create a YAML manifest including
Jobspec and KEDAtriggers(e.g., SQS, Kafka). - Example ScaledJob (SQS):
apiVersion: keda.sh/v1alpha1
kind: ScaledJob
metadata:
name: my-sqs-scaledjob
spec:
jobTargetRef:
template:
spec:
containers:
- name: worker
image: my-batch-processor:latest
env:
- name: SQS_QUEUE_NAME
value: my-queue
restartPolicy: Never
backoffLimit: 4
pollingInterval: 30 # Optional. Default: 30 seconds
successfulJobsHistoryLimit: 5 # Optional. Default: 10
failedJobsHistoryLimit: 5 # Optional. Default: 10
maxReplicaCount: 10 # Optional. Default: 100
triggers:
- type: aws-sqs-queue
metadata:
queueURL: https://sqs.us-east-1.amazonaws.com/123456789012/my-queue
queueLength: "5" # Scale a job for every 5 messages
awsRegion: "us-east-1"
identityOwner: "pod" # Use IRSA for authentication
Jobs, and HPA status.kubectl delete scaledjob my-sqs-scaledjob && helm uninstall keda --namespace kedaPrerequisites
Before we dive in, ensure you have the following tools and knowledge:
- Kubernetes Cluster: A running Kubernetes cluster (v1.16+). This could be Minikube, Kind, or a cloud-managed cluster (EKS, AKS, GKE).
- kubectl: The Kubernetes command-line tool, configured to connect to your cluster. You can find installation instructions on the official Kubernetes documentation.
- Helm: The Kubernetes package manager (v3+). Follow the Helm installation guide if you don’t have it.
- Basic Kubernetes Concepts: Familiarity with
Pods,Deployments,Jobs, andServices. - Access to a Message Queue (Optional but Recommended): While KEDA supports many triggers, a message queue like AWS SQS, Azure Service Bus, or Apache Kafka is ideal for demonstrating
ScaledJob. For this guide, we’ll use AWS SQS as an example. - Cloud Provider Credentials (if using cloud-specific triggers): For AWS SQS, you’ll need AWS credentials configured or, preferably, IAM Roles for Service Accounts (IRSA) setup on your EKS cluster. Similar mechanisms exist for Azure and GCP.
Step-by-Step Guide: Autoscaling Batch Workloads with KEDA ScaledJobs
Step 1: Install KEDA
KEDA is installed as a set of custom resource definitions (CRDs) and a controller within your Kubernetes cluster. The easiest and most recommended way to install KEDA is by using Helm. This will set up the KEDA operator and all necessary components to start scaling your applications.
First, add the KEDA Helm repository. Then, you can install KEDA into its own namespace, typically named keda. This ensures a clean separation of KEDA components from your application workloads. KEDA’s architecture is designed to be lightweight, extending the control plane rather than modifying your application containers, making it a powerful yet non-intrusive solution for autoscaling.
# Add the KEDA Helm repository
helm repo add kedacore https://kedacore.github.io/charts
# Update your Helm repositories
helm repo update
# Install KEDA into the 'keda' namespace
helm install keda kedacore/keda --namespace keda --create-namespace
Verify KEDA Installation
After installation, verify that the KEDA pods are running and healthy. You should see pods for the KEDA operator and the metrics server. The metrics server is crucial as it exposes the custom metrics that KEDA collects to the Horizontal Pod Autoscaler (HPA) and, indirectly, to ScaledJobs.
kubectl get pods --namespace keda
# Expected Output:
# NAME READY STATUS RESTARTS AGE
# keda-operator-56c87d46c4-abcde 1/1 Running 0 2m
# keda-operator-metrics-apiserver-7f65f745d-fghij 1/1 Running 0 2m
kubectl get crd | grep keda.sh
# Expected Output:
# clusteredscaledobjects.keda.sh 2023-10-27T10:00:00Z
# scaledjobs.keda.sh 2023-10-27T10:00:00Z
# scaledobjects.keda.sh 2023-10-27T10:00:00Z
The presence of scaledjobs.keda.sh in the CRD list confirms that KEDA is ready to manage your batch workloads.
Step 2: Prepare Your Batch Worker Application
For ScaledJobs, we need a container image that represents our batch processing logic. This application should be designed to process a single unit of work (e.g., one message from a queue) and then terminate gracefully. A common pattern is for the worker to read from a message queue, process the data, and then exit. The ScaledJob will then create new instances of this worker as needed.
Let’s create a simple Python worker that simulates processing messages from an SQS queue. This worker will print a message indicating it’s processing, simulate some work, and then exit. In a real-world scenario, it would interact with the SQS API to receive and delete messages.
Create a Python Worker Script (worker.py)
This script is simplified. A production worker would typically use boto3 to interact with SQS, receive messages, process them, and then delete them. For this example, we’ll just simulate the work.
# worker.py
import os
import time
import random
if __name__ == "__main__":
job_id = os.getenv("KUBERNETES_POD_NAME", "unknown-pod")
queue_name = os.getenv("SQS_QUEUE_NAME", "default-queue")
print(f"[{job_id}] Starting batch worker for queue: {queue_name}")
# Simulate processing a message
processing_time = random.uniform(5, 15)
print(f"[{job_id}] Simulating work for {processing_time:.2f} seconds...")
time.sleep(processing_time)
print(f"[{job_id}] Work finished. Exiting.")
Create a Dockerfile
This Dockerfile will build an image containing our Python worker.
# Dockerfile
FROM python:3.9-slim-buster
WORKDIR /app
COPY worker.py .
CMD ["python", "worker.py"]
Build and Push the Docker Image
Replace your-dockerhub-username with your Docker Hub username or the path to your container registry. If you’re using a private registry, ensure your Kubernetes cluster has credentials to pull from it.
docker build -t your-dockerhub-username/my-batch-processor:latest .
docker push your-dockerhub-username/my-batch-processor:latest
For more advanced container image security, consider integrating tools like Sigstore and Kyverno to sign and verify your images.
Step 3: Configure AWS SQS (or your chosen message queue)
For this example, we’ll use AWS SQS. You’ll need an SQS queue to act as the event source for KEDA. Create a standard SQS queue in your AWS account.
Create an SQS Queue
You can create an SQS queue via the AWS Management Console, AWS CLI, or CloudFormation/Terraform. For simplicity, we’ll assume you create it manually. Note down its URL and region.
Example SQS Queue URL: https://sqs.us-east-1.amazonaws.com/123456789012/my-keda-queue
Configure IAM Permissions for KEDA
KEDA needs permissions to read metrics from your SQS queue. The most secure way to do this in Kubernetes is by using IAM Roles for Service Accounts (IRSA) if you’re on EKS. If not, you might need to provide AWS credentials as a Kubernetes secret.
Option A: IAM Roles for Service Accounts (IRSA – Recommended for EKS)
1. Create an IAM policy that allows KEDA to read SQS queue attributes:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"sqs:GetQueueAttributes",
"sqs:ListQueues"
],
"Resource": "arn:aws:sqs:REGION:ACCOUNT_ID:my-keda-queue"
}
]
}
2. Create an IAM role and attach this policy. Configure a trust policy for the role to allow your Kubernetes service account to assume it. This involves specifying your OIDC provider URL.
3. Annotate the Kubernetes Service Account that your ScaledJob will use with the ARN of the IAM role. KEDA will automatically use this. We’ll define this Service Account in the next step.
Option B: Kubernetes Secret (Less Recommended for Production)
If IRSA isn’t an option, you can create a Kubernetes secret with your AWS access key and secret key. This secret would then be referenced in your ScaledJob definition. This method is generally less secure than IRSA.
# Replace with your actual AWS credentials
kubectl create secret generic aws-credentials \
--from-literal=AWS_ACCESS_KEY_ID='YOUR_AWS_ACCESS_KEY_ID' \
--from-literal=AWS_SECRET_ACCESS_KEY='YOUR_AWS_SECRET_ACCESS_KEY' \
--namespace default # Or your application namespace
For this guide, we’ll demonstrate using IRSA, as it’s the best practice for cloud environments.
Step 4: Define the ScaledJob
Now, let’s create the ScaledJob resource. This YAML manifest combines a Kubernetes Job specification with KEDA-specific scaling triggers. The ScaledJob defines how your batch worker should run and when KEDA should create new instances of it.
Key components of a ScaledJob:
jobTargetRef: This section is almost identical to a standard KubernetesJobspec, defining the pod template, restart policy (usuallyNeverfor batch jobs), and other job-specific settings likebackoffLimit.pollingInterval: How often KEDA should check the scaler (e.g., SQS queue) for new events.successfulJobsHistoryLimit/failedJobsHistoryLimit: How many completed/failed jobs to retain in Kubernetes history.maxReplicaCount: The maximum number of concurrentJobsKEDA can create. This acts as a safeguard to prevent resource exhaustion. Consider using Karpenter for efficient node autoscaling to support these bursty workloads.triggers: This is where you define the event source. KEDA supports a vast array of triggers, from message queues (SQS, Kafka, RabbitMQ) to databases, Prometheus metrics, and custom external scalers. Each trigger type has its own metadata for configuration.
Here’s a ScaledJob definition for our SQS worker, using IRSA for authentication:
# scaledjob-sqs.yaml
apiVersion: keda.sh/v1alpha1
kind: ScaledJob
metadata:
name: sqs-message-processor
namespace: default # Or your application namespace
spec:
jobTargetRef:
template:
spec:
serviceAccountName: keda-sqs-sa # Service Account with IRSA annotations
containers:
- name: worker
image: your-dockerhub-username/my-batch-processor:latest # Your image
imagePullPolicy: IfNotPresent
env:
- name: SQS_QUEUE_NAME
value: my-keda-queue # Your SQS queue name
restartPolicy: Never # Crucial for batch jobs
backoffLimit: 4 # Number of retries before marking job as failed
pollingInterval: 10 # Check SQS every 10 seconds
successfulJobsHistoryLimit: 5 # Retain 5 successful jobs
failedJobsHistoryLimit: 5 # Retain 5 failed jobs
maxReplicaCount: 10 # Max 10 concurrent jobs
triggers:
- type: aws-sqs-queue
metadata:
queueURL: https://sqs.us-east-1.amazonaws.com/123456789012/my-keda-queue # Your SQS Queue URL
queueLength: "1" # Create one job for every 1 message in the queue
awsRegion: "us-east-1" # Your AWS Region
identityOwner: "pod" # Use IRSA for authentication
# If not using IRSA, you'd use:
# awsAccessKeyIDFromEnv: AWS_ACCESS_KEY_ID
# awsSecretAccessKeyFromEnv: AWS_SECRET_ACCESS_KEY
# OR reference a secret:
# awsAccessKeyId:
# valueFrom:
# secretKeyRef:
# name: aws-credentials
# key: AWS_ACCESS_KEY_ID
# awsSecretAccessKey:
# valueFrom:
# secretKeyRef:
# name: aws-credentials
# key: AWS_SECRET_ACCESS_KEY
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: keda-sqs-sa
namespace: default
annotations:
# Replace with your actual OIDC provider and IAM Role ARN
eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/keda-sqs-processor-role
Important: Replace placeholders like your-dockerhub-username, my-keda-queue, 123456789012 (AWS Account ID), and us-east-1 with your specific values. Ensure the IAM Role ARN in the Service Account annotation is correct and has the necessary SQS permissions.
kubectl apply -f scaledjob-sqs.yaml
Verify ScaledJob Creation
Check if the ScaledJob and Service Account are created successfully.
kubectl get scaledjob sqs-message-processor
kubectl get sa keda-sqs-sa
# Expected Output:
# NAME AGE
# sqs-message-processor 10s
# NAME SECRETS AGE
# keda-sqs-sa 0 10s
Step 5: Test Autoscaling
Now that our ScaledJob is defined, let’s trigger it by sending messages to our SQS queue. KEDA will detect the messages and start creating Kubernetes Jobs.
Send Messages to SQS
You can use the AWS CLI to send messages to your SQS queue. Send multiple messages to observe scaling behavior.
# Replace with your SQS queue URL
aws sqs send-message --queue-url https://sqs.us-east-1.amazonaws.com/123456789012/my-keda-queue --message-body "message-1"
aws sqs send-message --queue-url https://sqs.us-east-1.amazonaws.com/123456789012/my-keda-queue --message-body "message-2"
aws sqs send-message --queue-url https://sqs.us-east-1.amazonaws.com/123456789012/my-keda-queue --message-body "message-3"
aws sqs send-message --queue-url https://sqs.us-east-1.amazonaws.com/123456789012/my-keda-queue --message-body "message-4"
aws sqs send-message --queue-url https://sqs.us-east-1.amazonaws.com/123456789012/my-keda-queue --message-body "message-5"
Observe KEDA Creating Jobs
Monitor the Kubernetes Jobs in your namespace. You should see new jobs being created as KEDA detects messages in the SQS queue.
kubectl get jobs --watch
# Expected Output (example, job names will vary):
# NAME COMPLETIONS DURATION AGE
# sqs-message-processor-abcde 0/1 0s 0s
# sqs-message-processor-fghij 0/1 0s 0s
# sqs-message-processor-klmno 0/1 0s 0s
# ... (more jobs as messages are processed)
# sqs-message-processor-abcde 1/1 12s 12s
# sqs-message-processor-fghij 1/1 10s 10s
# sqs-message-processor-klmno 1/1 15s 15s
You can also check the logs of the KEDA operator to see its scaling decisions:
kubectl logs -f deployment/keda-operator -n keda
The KEDA operator logs will show messages indicating it’s polling the SQS queue and creating/deleting jobs based on the queue length.
Check Pod Logs
Each job will create a pod. You can check the logs of these pods to see your worker script in action.
# Get a list of pods associated with your ScaledJob
kubectl get pods -l keda.sh/scaledjob-name=sqs-message-processor
# Get logs from one of the pods (replace with an actual pod name)
kubectl logs sqs-message-processor-abcde-xyz
You should see output similar to:
# Expected Output (from pod logs):
# [sqs-message-processor-abcde-xyz] Starting batch worker for queue: my-keda-queue
# [sqs-message-processor-abcde-xyz] Simulating work for 8.45 seconds...
# [sqs-message-processor-abcde-xyz] Work finished. Exiting.
Once all messages are processed and the queue is empty (and KEDA has polled it a few times), KEDA will stop creating new jobs, and the existing job pods will terminate after completing their work. The ScaledJob itself remains, ready to scale again when new events arrive.
Production Considerations
Deploying ScaledJobs in a production environment requires careful planning beyond just the basic setup:
- Resource Requests and Limits: Define appropriate CPU and memory requests and limits for your batch worker containers. This is crucial for scheduler efficiency and preventing resource exhaustion on nodes. Without them, your pods might get evicted or starve other critical services.
- Node Autoscaling: For bursty batch workloads, your cluster might need to scale its underlying nodes. Integrate with a node autoscaler like Karpenter or the Cluster Autoscaler. Karpenter is particularly effective for cost optimization as it can provision just-in-time nodes tailored to your workload’s specific requirements.
- Error Handling and Dead-Letter Queues (DLQs): Your batch worker must be resilient. Implement robust error handling, retries, and integrate with a Dead-Letter Queue (DLQ) for messages that repeatedly fail processing. This prevents poison pill messages from blocking your queue and ensures no data is lost.
- Monitoring and Alerting: Monitor KEDA’s health, the number of active
Jobs, queue lengths, and the success/failure rate of your batch workers. Integrate with your existing observability stack. Tools like eBPF Observability with Hubble can provide deep insights into network and application performance. Prometheus and Grafana are excellent choices for metrics and dashboards. - Security:
- IAM Roles for Service Accounts (IRSA): Always use IRSA (or equivalent for Azure/GCP) for cloud-provider authentication. Avoid embedding credentials directly in secrets if possible.
- Network Policies: Restrict network access for your batch workers using Kubernetes Network Policies. They should only be able to communicate with necessary services (e.g., message queue, database). Consider using CNI plugins like Cilium for advanced networking features, including WireGuard encryption for Pod-to-Pod traffic.
- Image Security: Scan your container images for vulnerabilities and use trusted base images. Implement image signing and verification with tools like Sigstore, possibly enforced by admission controllers like Kyverno.
- Job History Limits: Carefully set
successfulJobsHistoryLimitandfailedJobsHistoryLimitto balance diagnostic information with etcd storage overhead. Too many completed jobs can bloat your etcd database. - Concurrency and Throttling: Be mindful of the
maxReplicaCount. Ensure your backend services (databases, APIs) can handle the increased load when many batch workers are active simultaneously. Implement rate limiting or circuit breakers within your workers if necessary. - Graceful Shutdown: Design your worker application to handle Kubernetes termination signals (
SIGTERM) gracefully, allowing it to finish processing its current message before exiting. - Testing: Thoroughly test your
ScaledJobunder various load conditions, including peak load and zero load, to ensure it scales correctly and efficiently.
Troubleshooting
Here are some common issues you might encounter with KEDA ScaledJobs and their solutions:
-
Issue: KEDA is installed, but no Jobs are being created when messages are in the queue.
Solution:
- Check KEDA Operator Logs: The first place to look is the KEDA operator logs for errors related to polling the scaler or creating jobs.
kubectl logs -f deployment/keda-operator -n keda - Verify ScaledJob Status: Check the status of your
ScaledJob. It might contain useful warnings or error messages.kubectl describe scaledjob sqs-message-processor - Authentication Issues: Often, the problem is that KEDA cannot authenticate with the external system (e.g., AWS SQS).
- If using IRSA, double-check the Service Account annotations and the IAM role’s trust policy and permissions. Ensure the OIDC provider is correctly configured for your cluster.
- If using secrets, verify the secret exists in the correct namespace and the keys (
AWS_ACCESS_KEY_ID, etc.) match those expected by the KEDA trigger.
- Network Connectivity: Ensure KEDA (and your worker pods) can reach the external message queue. Check network policies if you have them enabled (refer to Kubernetes Network Policies).
- Scaler Configuration: Review the
metadatasection of your trigger in theScaledJob. A typo inqueueURL,awsRegion, orqueueLengthcan prevent scaling.
- Check KEDA Operator Logs: The first place to look is the KEDA operator logs for errors related to polling the scaler or creating jobs.
-
Issue: Jobs are created, but they immediately fail or get stuck.
Solution:
- Check Pod Logs: Get the logs of the failing job pods. This will almost always reveal the application-level error.
kubectl get pods -l keda.sh/scaledjob-name=sqs-message-processor kubectl logs <failing-pod-name> - Image Pull Issues: Ensure your container image can be pulled. Check
imagePullSecretsif using a private registry, and verify the image name/tag.kubectl describe pod <failing-pod-name> | grep -i 'imagepull' - Worker Application Errors: Your worker application might have bugs, incorrect environment variables, or missing dependencies. Test it locally if possible.
- Resource Constraints: The container might be OOMKilled (Out Of Memory Killed) or starved of CPU. Check pod events and adjust resource requests/limits.
- Check Pod Logs: Get the logs of the failing job pods. This will almost always reveal the application-level error.
-
Issue: KEDA creates too many or too few Jobs.
Solution:
queueLength/Target Value: Adjust thequeueLength(or equivalent for other scalers) in your trigger metadata. IfqueueLength: "1"means one job per message, and you have 100 messages, it will create 100 jobs (up tomaxReplicaCount). If you want fewer jobs, increase the target value (e.g.,queueLength: "10"for one job per 10 messages).maxReplicaCount: EnsuremaxReplicaCountin yourScaledJobis set appropriately. If it’s too low, KEDA won’t scale out further. If it’s too high, you might overwhelm your cluster or backend services.pollingInterval: A very long polling interval might cause slow scaling reactions. A very short one might put unnecessary load on the scaler. Balance this based on your latency requirements.
-
Issue: Completed Jobs are piling up in the cluster.
Solution:
- Adjust History Limits: Use
successfulJobsHistoryLimitandfailedJobsHistoryLimitin yourScaledJobspec. These values control how many completed/failed jobs Kubernetes retains. Set them to a reasonable number (e.g., 5-10) to keep your cluster clean while retaining enough history for debugging.spec: successfulJobsHistoryLimit: 5 failedJobsHistoryLimit: 5
- Adjust History Limits: Use
-
Issue: KEDA is not scaling down to zero.
Solution:
- Queue Empty Check: Ensure your worker correctly processes and deletes messages from the queue. If messages are not deleted, KEDA will continuously see messages and try to scale.
pollingInterval: It takes at least onepollingIntervalafter the queue is empty for KEDA to detect zero messages and stop creating jobs.cooldownPeriod(forScaledObject, not directlyScaledJob): WhileScaledJobdoesn’t have a directcooldownPeriodlikeScaledObject, the overall scaling down behavior is influenced by polling. Ensure your queue is truly empty and KEDA’s logs confirm it sees zero messages.
FAQ Section
-
What is the difference between
ScaledJobandScaledObject?ScaledObjectis used for autoscaling long-running workloads likeDeploymentsandStatefulSets. It adjusts the number of replicas of these resources.ScaledJob, on the other hand, is specifically designed for batch workloads. It dynamically creates and manages KubernetesJobsbased on event sources, where each job typically processes a unit of work and then terminates. Think ofScaledObjectfor services andScaledJobfor tasks. -
Can I use any Kubernetes
Jobspec within aScaledJob?Yes, the
jobTargetRef.templatesection of aScaledJobis essentially a standard KubernetesPodTemplateSpecfrom aJob. This means you can leverage all the features of a regular Kubernetes Job, including init containers, sidecars, volumes, resource requests/limits, and more. The only strict requirement for batch processing is typicallyrestartPolicy: Never. -
How does KEDA handle authentication to external systems like AWS SQS or Azure Service Bus?
KEDA offers several methods for authentication, prioritizing secure cloud-native approaches:
- Pod Identity: Recommended for cloud providers (e.g., AWS IRSA, Azure AD Workload Identity, GCP Workload Identity). This allows a Kubernetes Service Account to assume an IAM role/identity.
- Kubernetes Secrets: Credentials (e.g., API keys, connection strings) can be stored in Kubernetes secrets and referenced by the KEDA trigger.
- Environment Variables: Less secure, but possible.
Always prefer pod identity where available for
