Beyond Basic Orchestration: Mastering Kubernetes for High-Availability Laravel Deployments on AWS
Leveraging AWS EKS for Resilient Laravel Applications
Deploying a Laravel application on Kubernetes, specifically Amazon Elastic Kubernetes Service (EKS), offers significant advantages in terms of scalability, resilience, and manageability. This guide moves beyond basic containerization to explore advanced patterns for achieving high availability, focusing on stateful services, robust networking, and automated recovery mechanisms within the AWS ecosystem.
Database High Availability with RDS and Persistent Volumes
For a production Laravel application, relying on ephemeral storage for your database is a non-starter. AWS Relational Database Service (RDS) is the de facto standard for managed relational databases, offering multi-AZ deployments for automatic failover and read replicas for scaling read traffic. However, if your architecture necessitates running your database *within* Kubernetes (e.g., for specific latency requirements or complex state management), you must implement robust persistent storage solutions.
When using EKS, the AWS EBS CSI driver is crucial. This driver allows Kubernetes to provision and manage Amazon Elastic Block Store (EBS) volumes dynamically. For high availability, you’ll want to configure your StatefulSets to use StorageClasses that support multi-AZ or, at minimum, ensure your EBS volumes are backed by resilient storage like `gp3` or `io2` and are provisioned in the correct AWS Availability Zone(s) corresponding to your EKS worker nodes.
StatefulSet Configuration for Databases
Consider a PostgreSQL deployment within Kubernetes. A typical StatefulSet definition would look like this:
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: postgresql
namespace: laravel-prod
spec:
serviceName: postgresql
replicas: 3 # For potential HA, though DB replication is key
selector:
matchLabels:
app: postgresql
template:
metadata:
labels:
app: postgresql
spec:
containers:
- name: postgresql
image: postgres:14-alpine
ports:
- containerPort: 5432
name: tcp-postgresql
env:
- name: POSTGRES_USER
valueFrom:
secretKeyRef:
name: postgresql-secret
key: user
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: postgresql-secret
key: password
- name: POSTGRES_DB
value: myappdb
volumeMounts:
- name: postgresql-persistent-storage
mountPath: /var/lib/postgresql/data
volumeClaimTemplates:
- metadata:
name: postgresql-persistent-storage
spec:
accessModes: [ "ReadWriteOnce" ] # Important: EBS volumes are typically RWO
storageClassName: "gp3-csi" # Or your preferred CSI StorageClass
resources:
requests:
storage: 100Gi
Note: For true database HA, you’ll need to implement database-level replication (e.g., PostgreSQL streaming replication, MySQL replication) *between* these pods. Kubernetes itself doesn’t magically make a single-instance database highly available. The StatefulSet ensures stable network identities and ordered deployment/scaling, which are prerequisites for replication setup.
Caching and Session Management with ElastiCache
Laravel’s caching and session drivers often rely on Redis or Memcached. For production, AWS ElastiCache is the managed solution. Deploying ElastiCache outside of Kubernetes simplifies management, as these are typically stateless services that don’t require the same level of orchestration as your application pods.
ElastiCache Redis Configuration
When configuring your Laravel application (running in EKS) to connect to ElastiCache, ensure your EKS cluster’s VPC and subnets are configured to allow outbound traffic to the ElastiCache security group. You’ll typically use a Redis cluster mode enabled cluster for better performance and availability.
// config/cache.php
'redis' => [
'driver' => 'redis',
'connection' => 'default',
'redis' => [
'client' => 'phpredis',
'cluster' => env('REDIS_CLUSTER', 'default'), // Set to 'redis' if using cluster mode
'host' => env('REDIS_HOST', 'localhost'),
'password' => env('REDIS_PASSWORD', null),
'port' => env('REDIS_PORT', 6379),
'database' => env('REDIS_DB', 0),
],
],
// config/session.php
'driver' => env('SESSION_DRIVER', 'file'),
'lottery' => [
'minutes' => 60,
'attempts' => 100,
],
'cookie' => env(
'SESSION_COOKIE',
Str::slug(env('APP_NAME', 'laravel'), '_').'_session'
),
'path' => '/',
'domain' => env('SESSION_DOMAIN', null),
'secure' => env('SESSION_SECURE_COOKIE', false),
'http_only' => true,
'same_site' => 'lax',
'redis' => [
'driver' => 'redis',
'connection' => 'default',
'redis' => [
'client' => 'phpredis',
'cluster' => env('REDIS_CLUSTER', 'default'),
'host' => env('REDIS_HOST', 'localhost'),
'password' => env('REDIS_PASSWORD', null),
'port' => env('REDIS_PORT', 6379),
'database' => env('REDIS_DB', 1), // Typically a different DB for sessions
],
],
Ensure your Kubernetes deployment’s ServiceAccount has the necessary IAM permissions to access AWS resources if you are using IAM roles for service accounts (IRSA) for authentication with ElastiCache (though direct endpoint access via security groups is more common).
Ingress and Load Balancing with AWS Load Balancer Controller
For external traffic to reach your Laravel application pods, you need an Ingress controller. The AWS Load Balancer Controller is the recommended solution for EKS. It provisions and manages AWS Application Load Balancers (ALBs) or Network Load Balancers (NLBs) based on Kubernetes Ingress resources.
Deploying the AWS Load Balancer Controller
First, you need to create an IAM OIDC provider for your EKS cluster and an IAM policy that grants the controller permissions to manage load balancers. Then, deploy the controller itself:
# 1. Create IAM Policy (example, refer to AWS docs for latest) aws iam create-policy --policy-name AWSLoadBalancerControllerIAMPolicy --policy-document file://iam_policy.json # 2. Deploy the controller (using Helm is common) helm repo add aws-load-balancer-controller https://aws.github.io/eks-charts helm install aws-load-balancer-controller aws-load-balancer-controller/aws-load-balancer-controller \ -n kube-system \ --set clusterName=your-eks-cluster-name \ --set serviceAccount.create=false \ --set serviceAccount.name=aws-load-balancer-controller \ --set region=us-east-1 \ --set vpcId=your-vpc-id
Kubernetes Ingress Resource for Laravel
Once deployed, you can define an Ingress resource to route traffic to your Laravel application’s Service. This Ingress resource will trigger the AWS Load Balancer Controller to provision an ALB.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: laravel-ingress
namespace: laravel-prod
annotations:
kubernetes.io/ingress.class: alb
alb.ingress.kubernetes.io/scheme: internet-facing
alb.ingress.kubernetes.io/target-type: ip
alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 80}, {"HTTPS":443}]'
alb.ingress.kubernetes.io/certificate-arn: arn:aws:acm:us-east-1:123456789012:certificate/your-certificate-id
alb.ingress.kubernetes.io/ssl-redirect: '443' # Redirect HTTP to HTTPS
spec:
rules:
- http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: laravel-app-service # Your Laravel app's Kubernetes Service
port:
number: 80
This configuration automatically provisions an ALB, sets up listeners for HTTP and HTTPS (using an ACM certificate), and configures target groups pointing to your Laravel application pods. The `target-type: ip` is generally preferred for EKS with the ALB controller.
Application Health Checks and Auto-Scaling
High availability hinges on the ability to detect and recover from failures. Kubernetes provides Liveness and Readiness probes for this purpose. For Laravel applications, these probes need to be intelligently designed.
Liveness and Readiness Probes for Laravel
A simple HTTP GET request to the application’s root (`/`) might not be sufficient. A more robust probe would check a dedicated health endpoint that performs a quick check of critical dependencies (e.g., database connectivity, cache availability).
// In your Laravel app, create a route and controller for health checks
// routes/api.php
Route::get('/health', [\App\Http\Controllers\HealthCheckController::class, 'index']);
// app/Http/Controllers/HealthCheckController.php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
class HealthCheckController extends Controller
{
public function index()
{
try {
// Check database connection
DB::connection()->getPdo();
// Check cache connection (e.g., Redis)
Cache::store('redis')->get('health_check_key'); // Simple cache get
Cache::store('redis')->put('health_check_key', 'ok', 1); // Simple cache put
return response()->json(['status' => 'ok', 'message' => 'All systems operational.']);
} catch (\Exception $e) {
Log::error("Health check failed: " . $e->getMessage());
return response()->json(['status' => 'error', 'message' => 'Dependency unavailable.'], 503);
}
}
}
apiVersion: apps/v1
kind: Deployment
metadata:
name: laravel-app
namespace: laravel-prod
spec:
replicas: 3
selector:
matchLabels:
app: laravel-app
template:
metadata:
labels:
app: laravel-app
spec:
containers:
- name: laravel-app
image: your-docker-repo/laravel-app:latest
ports:
- containerPort: 8000 # Or whatever port your app listens on
livenessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 15
periodSeconds: 20
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 2
# ... other container configurations (env vars, volumes, etc.)
The livenessProbe tells Kubernetes when to restart a container. If the probe fails, Kubernetes kills the container, and the kubelet restarts it. The readinessProbe tells Kubernetes when a container is ready to serve traffic. If the probe fails, the pod is removed from the Service’s endpoints, preventing traffic from being sent to it until it becomes ready again. This is crucial for zero-downtime deployments and graceful handling of application startup or temporary issues.
Horizontal Pod Autoscaler (HPA)
To automatically scale your Laravel application based on load, configure an HPA. This will adjust the number of application pods based on CPU or memory utilization.
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: laravel-app-hpa
namespace: laravel-prod
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: laravel-app
minReplicas: 3 # Ensure a minimum of 3 replicas for HA
maxReplicas: 15
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70 # Scale up when CPU utilization reaches 70%
# You can also add memory metrics
# - type: Resource
# resource:
# name: memory
# target:
# type: Utilization
# averageUtilization: 75
For effective HPA, ensure your pods have appropriate CPU and memory requests and limits defined in their Deployment manifests. Without these, Kubernetes cannot accurately calculate utilization percentages.
Background Jobs and Queues with SQS and SupervisorD
Laravel’s queue system is vital for offloading long-running tasks. For high availability and scalability, AWS Simple Queue Service (SQS) is an excellent choice as a queue driver. For processing these jobs, you’ll need dedicated worker pods.
SQS Queue Configuration
Configure your .env file and config/queue.php to use the SQS driver. Ensure the IAM role associated with your EKS worker nodes (or the pod’s ServiceAccount via IRSA) has permissions to interact with SQS (e.g., sqs:SendMessage, sqs:ReceiveMessage, sqs:DeleteMessage, sqs:GetQueueAttributes).
# .env QUEUE_CONNECTION=sqs AWS_ACCESS_KEY_ID= # Not needed if using IAM roles AWS_SECRET_ACCESS_KEY= # Not needed if using IAM roles AWS_DEFAULT_REGION=us-east-1 SQS_QUEUE_BASE_URL=https://sqs.us-east-1.amazonaws.com/123456789012/my-laravel-queue
// config/queue.php (relevant section)
'connections' => [
// ... other connections
'sqs' => [
'driver' => 'sqs',
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
'queue' => env('SQS_QUEUE_BASE_URL', 'sqs.us-east-1.amazonaws.com/your-account-id/your-queue-name'),
'after_commit' => false,
],
// ...
],
Dedicated Worker Deployment
Create a separate Deployment for your Laravel workers. These pods will run the php artisan queue:listen or php artisan queue:work command. To ensure workers don’t die unexpectedly and to manage the process lifecycle robustly, using SupervisorD within the worker container is highly recommended.
# Dockerfile for worker
FROM php:8.2-fpm-alpine
# Install dependencies for Supervisor, PHP extensions, etc.
RUN apk add --no-cache supervisor \
&& docker-php-ext-install pdo pdo_mysql \
&& pecl install redis \
&& docker-php-ext-enable redis \
&& apk del --no-cache $PHPIZE_DEPS
# Copy Laravel application and Supervisor config
COPY --chown=www-data:www-data . /var/www/html
COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf
# Set working directory and user
WORKDIR /var/www/html
USER www-data
# Expose port if needed (though workers typically don't need exposed ports)
# EXPOSE 8000
# Command to run Supervisor
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/supervisord.conf"]
# supervisord.conf [supervisord] nodaemon=true user=root [program:laravel-worker] process_name=%(program_name)s_%(process_num)02d command=php artisan queue:work sqs --tries=3 --timeout=300 --sleep=5 --daemon autostart=true autorestart=true user=www-data numprocs=4 # Adjust based on your pod's CPU/memory and desired concurrency redirect_stderr=true stdout_logfile=/dev/stdout stderr_logfile=/dev/stderr stdout_logfile_maxbytes=0 stderr_logfile_maxbytes=0
apiVersion: apps/v1
kind: Deployment
metadata:
name: laravel-worker
namespace: laravel-prod
spec:
replicas: 3 # Scale workers independently based on queue depth
selector:
matchLabels:
app: laravel-worker
template:
metadata:
labels:
app: laravel-worker
spec:
containers:
- name: laravel-worker
image: your-docker-repo/laravel-worker:latest
envFrom:
- configMapRef:
name: app-config # Contains .env variables
command: ["/usr/bin/supervisord", "-c", "/etc/supervisor/supervisord.conf"]
resources:
requests:
cpu: "500m"
memory: "1Gi"
limits:
cpu: "1000m"
memory: "2Gi"
# Consider using affinity/anti-affinity rules to spread workers across nodes/AZs
You can also implement a Kubernetes HorizontalPodAutoscaler for the worker deployment, scaling based on SQS queue depth (requires custom metrics adapter or a tool like KEDA – Kubernetes Event-Driven Autoscaling).
Logging and Monitoring
Robust logging and monitoring are non-negotiable for production systems. For EKS, consider a centralized logging solution like the EFK stack (Elasticsearch, Fluentd, Kibana) or Loki/Promtail/Grafana. For metrics, Prometheus and Grafana are standard.
Fluentd for Log Aggregation
Deploying Fluentd as a DaemonSet on your EKS nodes allows it to collect logs from all containers and forward them to your chosen backend (e.g., Elasticsearch, CloudWatch Logs).
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: fluentd
namespace: logging
labels:
app: fluentd
spec:
selector:
matchLabels:
app: fluentd
template:
metadata:
labels:
app: fluentd
spec:
containers:
- name: fluentd
image: fluent/fluentd:v1.16-debian
resources:
limits:
cpu: 200m
memory: 200Mi
requests:
cpu: 100m
memory: 100Mi
volumeMounts:
- name: varlog
mountPath: /var/log
- name: varlibdockercontainers
mountPath: /var/lib/docker/containers
readOnly: true
volumes:
- name: varlog
hostPath:
path: /var/log
- name: varlibdockercontainers
hostPath:
path: /var/lib/docker/containers
You’ll need a Fluentd configuration file (e.g., /fluentd/etc/fluent.conf) to define input sources (tailing container logs) and output destinations (e.g., Elasticsearch HTTP endpoint).
Conclusion
Achieving high availability for Laravel on EKS involves a multi-faceted approach. By leveraging managed AWS services like RDS and ElastiCache, implementing robust Kubernetes patterns for stateful applications and stateless services, and configuring intelligent health checks and auto-scaling, you can build resilient, scalable, and self-healing deployments. Continuous monitoring and iterative refinement of these configurations are key to maintaining a production-ready environment.