Beyond Basic Containers: Mastering Kubernetes for High-Availability Laravel Deployments on AWS EKS
Establishing a Robust EKS Cluster for Laravel
Deploying a high-availability Laravel application on AWS EKS requires more than just a basic container setup. We need to architect for resilience, scalability, and efficient resource utilization. This begins with a well-configured EKS cluster. For production environments, we’ll leverage managed Node Groups for automated scaling and patching, and ensure proper networking with VPC CNI.
First, let’s define the core EKS cluster configuration. We’ll use `eksctl`, the official CLI for Amazon EKS, for its simplicity and adherence to best practices.
EKS Cluster Provisioning with `eksctl`
Create a cluster configuration file (e.g., `eks-cluster.yaml`) to define your EKS cluster. This example sets up a cluster with a managed Node Group, enabling auto-scaling and specifying instance types suitable for web applications.
Key considerations here include:
region: Your AWS region.version: Kubernetes version. Keep it updated.nodeGroups: Defines the worker nodes. We’re using a managed Node Group for ease of management.desiredCapacity,minSize,maxSize: Configure Auto Scaling Group parameters for your nodes.instanceType: Choose instance types that balance cost and performance (e.g.,t3.mediumorm5.large).privateNetworking: Crucial for security. Nodes should not be directly exposed to the public internet.
apiVersion: eksctl.io/v1alpha5
kind: ClusterConfig
metadata:
name: laravel-prod-cluster
region: us-east-1
version: "1.28" # Specify your desired Kubernetes version
managedNodeGroups:
- name: laravel-workers
instanceType: t3.medium
desiredCapacity: 2
minSize: 1
maxSize: 5
volumeSize: 80 # GiB
ssh:
allow: false # Disable SSH for managed nodes in production
privateNetworking: true
tags:
nodegroup: laravel-workers
environment: production
iam:
withAddonPolicies:
autoScaler: true # Enable IAM policy for cluster-autoscaler
cloudWatch: true # Enable IAM policy for CloudWatch logging
albIngress: true # Enable IAM policy for AWS Load Balancer Controller
To provision the cluster, execute the following command:
eksctl create cluster -f eks-cluster.yaml
This process can take 15-20 minutes. Once complete, `eksctl` will update your ~/.kube/config file to point to the new cluster.
Containerizing the Laravel Application
A well-structured Dockerfile is essential for efficient and secure containerization. For Laravel, we typically need PHP, an web server (Nginx or Apache), and potentially other extensions. Using a multi-stage build is a best practice to keep the final image lean.
Optimized Dockerfile for Laravel
This Dockerfile uses an official PHP image, installs necessary extensions, copies application code, runs composer install, and then uses a lean Nginx image for serving static assets and proxying to PHP-FPM.
# Stage 1: Build the application
FROM php:8.2-fpm AS builder
# Install system dependencies
RUN apt-get update && apt-get install -y \
git \
unzip \
libzip-dev \
libpng-dev \
libjpeg-dev \
libfreetype6-dev \
libssl-dev \
libonig-dev \
libxml2-dev \
libicu-dev \
libxslt1-dev \
zlib1g-dev \
acl \
vim \
cron \
supervisor \
&& rm -rf /var/lib/apt/lists/*
# Install PHP extensions
RUN docker-php-ext-configure gd --with-freetype --with-jpeg && docker-php-ext-install gd
RUN docker-php-ext-install pdo pdo_mysql zip intl bcmath opcache sockets xsl
# Install Composer
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
# Set working directory
WORKDIR /var/www/html
# Copy application files
COPY . .
# Install dependencies
RUN composer install --no-dev --optimize-autoloader --no-interaction
# Clear cache
RUN rm -rf var/cache/* var/log/* bootstrap/cache/*
# Copy compiled assets (if using Laravel Mix/Vite)
# RUN npm install && npm run build
# Stage 2: Production image with Nginx
FROM nginx:alpine
# Install PHP-FPM and required extensions for runtime
RUN apk add --no-cache \
php82 \
php82-fpm \
php82-pdo \
php82-pdo_mysql \
php82-zip \
php82-intl \
php82-bcmath \
php82-opcache \
php82-sockets \
php82-xsl \
libzip \
libpng \
libjpeg-turbo \
freetype2 \
openssl \
oniguruma \
libxml2 \
icu-data-full \
libxslt \
zlib \
acl \
vim \
cronie \
supervisor
# Copy application code from builder stage
COPY --from=builder /var/www/html /var/www/html
# Copy Nginx configuration
COPY docker/nginx/default.conf /etc/nginx/conf.d/default.conf
# Copy PHP-FPM configuration
COPY docker/php-fpm/www.conf /etc/php82/php-fpm.d/www.conf
# Copy Supervisor configuration
COPY docker/supervisor/supervisord.conf /etc/supervisor/conf.d/supervisord.conf
# Create necessary directories and set permissions
RUN mkdir -p /var/www/html/storage/framework/sessions \
&& mkdir -p /var/www/html/storage/framework/views \
&& mkdir -p /var/www/html/storage/framework/cache \
&& mkdir -p /var/www/html/bootstrap/cache \
&& chown -R www-data:www-data /var/www/html/storage \
&& chown -R www-data:www-data /var/www/html/bootstrap/cache
# Expose port 80
EXPOSE 80
# Start supervisor to manage Nginx and PHP-FPM
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/supervisord.conf"]
You’ll also need corresponding Nginx and PHP-FPM configuration files. For example, docker/nginx/default.conf:
server {
listen 80;
server_name localhost;
root /var/www/html/public;
index index.php index.html index.htm;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
try_files $uri =404;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass php-fpm:9000; # Assuming php-fpm service name
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
location ~ /\.ht {
deny all;
}
# Serve static assets directly from storage/app/public if configured
location /storage/ {
alias /var/www/html/storage/app/public/;
expires 30d;
add_header Cache-Control "public";
}
}
And docker/php-fpm/www.conf (simplified):
[global] error_log = /var/log/php-fpm.log daemonize = no [www] user = www-data group = www-data listen = 9000 listen.owner = www-data listen.group = www-data listen.mode = 0660 pm = dynamic pm.max_children = 10 pm.start_servers = 2 pm.min_spare_servers = 1 pm.max_spare_servers = 5 pm.process_idle_timeout = 10s request_terminate_timeout = 60s
Finally, docker/supervisor/supervisord.conf to manage Nginx and PHP-FPM processes:
[supervisord] nodaemon=true user=root [program:nginx] command=/usr/sbin/nginx -g "daemon off;" autorestart=true priority=10 [program:php-fpm] command=/usr/sbin/php-fpm82 --nodaemonize --fpm-config /etc/php82/php-fpm.conf autorestart=true priority=20 stdout_logfile=/dev/stdout stdout_logfile_maxbytes=0 stderr_logfile=/dev/stderr stderr_logfile_maxbytes=0
Build and push your Docker image to Amazon ECR (Elastic Container Registry).
# Configure AWS CLI and Docker for ECR aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin [YOUR_AWS_ACCOUNT_ID].dkr.ecr.us-east-1.amazonaws.com # Create ECR repository (if it doesn't exist) aws ecr create-repository --repository-name laravel-app --region us-east-1 --image-scanning-configuration scanOnPush=true --image-tag-mutability MUTABLE # Build and push the image docker build -t laravel-app:latest . docker tag laravel-app:latest [YOUR_AWS_ACCOUNT_ID].dkr.ecr.us-east-1.amazonaws.com/laravel-app:latest docker push [YOUR_AWS_ACCOUNT_ID].dkr.ecr.us-east-1.amazonaws.com/laravel-app:latest
Kubernetes Manifests for High Availability
Now, let’s define the Kubernetes resources. We’ll need Deployments for our application, Services for internal and external access, an Ingress for HTTP routing, PersistentVolumes for stateful data, and potentially Jobs for migrations and scheduled tasks.
Deployment for Laravel Application
The Deployment manages stateless application pods. We’ll configure readiness and liveness probes, resource requests/limits, and horizontal pod autoscaling (HPA).
apiVersion: apps/v1
kind: Deployment
metadata:
name: laravel-app-deployment
labels:
app: laravel-app
spec:
replicas: 3 # Start with 3 replicas for HA
selector:
matchLabels:
app: laravel-app
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 1 # Allow one pod to be unavailable during updates
maxSurge: 1 # Allow one extra pod to be created
template:
metadata:
labels:
app: laravel-app
spec:
containers:
- name: laravel-app
image: [YOUR_AWS_ACCOUNT_ID].dkr.ecr.us-east-1.amazonaws.com/laravel-app:latest # Your ECR image
ports:
- containerPort: 80
env:
- name: APP_ENV
value: "production"
- name: APP_LOG_LEVEL
value: "info"
- name: DB_HOST
valueFrom:
secretKeyRef:
name: laravel-db-secrets
key: DB_HOST
- name: DB_PORT
valueFrom:
secretKeyRef:
name: laravel-db-secrets
key: DB_PORT
- name: DB_DATABASE
valueFrom:
secretKeyRef:
name: laravel-db-secrets
key: DB_DATABASE
- name: DB_USERNAME
valueFrom:
secretKeyRef:
name: laravel-db-secrets
key: DB_USERNAME
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: laravel-db-secrets
key: DB_PASSWORD
- name: CACHE_DRIVER
value: "redis"
- name: SESSION_DRIVER
value: "redis"
- name: QUEUE_CONNECTION
value: "redis"
readinessProbe:
httpGet:
path: /healthz # A simple health check endpoint in Laravel
port: 80
initialDelaySeconds: 15
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
livenessProbe:
httpGet:
path: /healthz
port: 80
initialDelaySeconds: 30
periodSeconds: 20
timeoutSeconds: 5
failureThreshold: 5
resources:
requests:
cpu: "200m"
memory: "256Mi"
limits:
cpu: "500m"
memory: "512Mi"
# Optional: If using persistent storage for logs or uploads
# volumes:
# - name: storage-volume
# persistentVolumeClaim:
# claimName: laravel-storage-pvc
# volumeMounts:
# - name: storage-volume
# mountPath: /var/www/html/storage
Create a simple health check route in your Laravel application (e.g., in routes/web.php or routes/api.php):
Route::get('/healthz', function () {
// Basic check: ensure database connection is possible
try {
DB::connection()->getPdo();
return response('OK', 200);
} catch (\Exception $e) {
return response('Database connection failed', 500);
}
});
Horizontal Pod Autoscaler (HPA)
Configure HPA to automatically scale the number of application pods based on CPU utilization.
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: laravel-app-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: laravel-app-deployment
minReplicas: 2 # Minimum number of pods
maxReplicas: 10 # Maximum number of pods
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70 # Scale up when CPU utilization reaches 70%
# Optional: Scale based on memory
# - type: Resource
# resource:
# name: memory
# target:
# type: Utilization
# averageUtilization: 70
Service for Internal Communication
A ClusterIP Service provides a stable internal IP address for pods managed by the Deployment.
apiVersion: v1
kind: Service
metadata:
name: laravel-app-service
spec:
selector:
app: laravel-app
ports:
- protocol: TCP
port: 80
targetPort: 80
type: ClusterIP
Ingress for External Access
We’ll use the AWS Load Balancer Controller to provision an Application Load Balancer (ALB) that routes external traffic to our Laravel application. Ensure the controller is installed in your EKS cluster.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: laravel-ingress
annotations:
kubernetes.io/ingress.class: alb
alb.ingress.kubernetes.io/scheme: internet-facing
alb.ingress.kubernetes.io/target-type: ip # Use IP mode for better performance with EKS
alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 80}, {"HTTPS": 443}]'
alb.ingress.kubernetes.io/certificate-arn: arn:aws:acm:us-east-1:[YOUR_AWS_ACCOUNT_ID]:certificate/[YOUR_CERTIFICATE_ID] # Replace with your ACM certificate ARN
alb.ingress.kubernetes.io/ssl-redirect: "true" # Redirect HTTP to HTTPS
spec:
rules:
- http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: laravel-app-service
port:
number: 80
Note: Replace [YOUR_AWS_ACCOUNT_ID] and [YOUR_CERTIFICATE_ID] with your actual AWS account ID and ACM certificate ARN. Ensure the AWS Load Balancer Controller is installed and configured correctly in your EKS cluster.
Managing State and Background Jobs
Laravel applications often rely on databases, caching, and queues. For high availability, these services should also be managed for resilience.
Database and Cache with AWS RDS and ElastiCache
Instead of running databases or Redis within Kubernetes pods (which is generally discouraged for stateful services in production), leverage managed AWS services:
- Database: Use Amazon RDS (e.g., Aurora PostgreSQL or MySQL). Configure it for Multi-AZ deployment for high availability.
- Cache/Queue: Use Amazon ElastiCache for Redis. Configure a Redis cluster for high availability.
Store the connection details (host, port, username, password, database name) in Kubernetes Secrets. Create a secret named laravel-db-secrets:
apiVersion: v1 kind: Secret metadata: name: laravel-db-secrets type: Opaque data: DB_HOST: [BASE64_ENCODED_RDS_ENDPOINT] DB_PORT: [BASE64_ENCODED_RDS_PORT] DB_DATABASE: [BASE64_ENCODED_DB_NAME] DB_USERNAME: [BASE64_ENCODED_DB_USERNAME] DB_PASSWORD: [BASE64_ENCODED_DB_PASSWORD] REDIS_HOST: [BASE64_ENCODED_ELASTICACHE_ENDPOINT] REDIS_PORT: [BASE64_ENCODED_ELASTICACHE_PORT]
To encode values, use:
echo -n "your_value" | base64
Redis Queue Workers
For background job processing, deploy dedicated worker pods. These can be managed by a separate Deployment and Service.
apiVersion: apps/v1
kind: Deployment
metadata:
name: laravel-worker-deployment
labels:
app: laravel-worker
spec:
replicas: 2 # Start with 2 replicas for HA
selector:
matchLabels:
app: laravel-worker
template:
metadata:
labels:
app: laravel-worker
spec:
containers:
- name: laravel-worker
image: [YOUR_AWS_ACCOUNT_ID].dkr.ecr.us-east-1.amazonaws.com/laravel-app:latest # Same image, different command
command: ["php", "artisan", "queue:work", "--tries=3", "--timeout=300"] # Command to run queue worker
envFrom:
- secretRef:
name: laravel-db-secrets # Inherit DB and Redis credentials
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "300m"
memory: "256Mi"
restartPolicy: Always
Ensure your config/queue.php and .env files are correctly configured to use Redis and the credentials from the secrets.
Database Migrations and Scheduled Tasks
Running database migrations and scheduled tasks (cron jobs) requires careful orchestration within Kubernetes.
Running Migrations with Kubernetes Jobs
Use Kubernetes Jobs to run one-off tasks like database migrations. This ensures the migration runs to completion before new application versions are deployed.
apiVersion: batch/v1
kind: Job
metadata:
name: laravel-migration-job
spec:
template:
spec:
containers:
- name: laravel-migration
image: [YOUR_AWS_ACCOUNT_ID].dkr.ecr.us-east-1.amazonaws.com/laravel-app:latest # Same image
command: ["php", "artisan", "migrate", "--force"] # --force is crucial for production
envFrom:
- secretRef:
name: laravel-db-secrets # Inherit DB credentials
restartPolicy: Never # Or OnFailure, depending on desired behavior
backoffLimit: 4 # Number of retries before marking the job as failed
You can trigger this job manually using kubectl apply -f migration-job.yaml or integrate it into your CI/CD pipeline. For automated deployments, consider using a tool like Argo CD or Flux CD, which can manage the application of migration jobs before deploying new application versions.
Scheduled Tasks with CronJobs
Laravel’s scheduler can be managed using Kubernetes CronJobs. This approach replaces traditional cron daemons on individual servers.
apiVersion: batch/v1
kind: CronJob
metadata:
name: laravel-scheduler
spec:
schedule: "*/5 * * * *" # Run every 5 minutes
jobTemplate:
spec:
template:
spec:
containers:
- name: laravel-scheduler
image: [YOUR_AWS_ACCOUNT_ID].dkr.ecr.us-east-1.amazonaws.com/laravel-app:latest # Same image
command: ["php", "artisan", "schedule:run"]
envFrom:
- secretRef:
name: laravel-db-secrets # Inherit DB credentials
restartPolicy: OnFailure
concurrencyPolicy: Forbid # Prevents concurrent runs if a previous job is still running
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 1
This CronJob will execute php artisan schedule:run every 5 minutes, which in turn triggers any defined scheduled commands in your Laravel application.
Monitoring, Logging, and Security Best Practices
A production-ready deployment requires robust monitoring, centralized logging, and strong security practices.
Centralized Logging with CloudWatch Container Insights
Leverage AWS CloudWatch Container Insights to aggregate logs from your EKS pods. This involves deploying the CloudWatch agent as a DaemonSet.
Ensure your EKS cluster has the necessary IAM permissions for the CloudWatch agent. You can typically enable this during cluster creation with `eksctl` using the cloudWatch: true option in the Node Group configuration.
Configure your Laravel application to log to stdout and stderr. The CloudWatch agent will then collect these logs.
Monitoring with Prometheus and Grafana
For more detailed application and cluster metrics, consider deploying Prometheus and Grafana within your EKS cluster. You can use the Prometheus Operator for easier management.
Expose metrics from your Laravel application (e.g., using a package like prometheus-php/client) and configure Prometheus to scrape them. Grafana can then be used to visualize these metrics.
Security Considerations
- Network Policies: Implement Kubernetes Network Policies to restrict traffic between pods, enforcing the principle of least privilege.
- Secrets Management: Use Kubernetes Secrets for sensitive information. For enhanced security, integrate with AWS Secrets Manager or HashiCorp Vault.
- IAM Roles for Service Accounts (IRSA): Grant specific AWS permissions to your Kubernetes pods using IRSA, avoiding the need to embed AWS credentials in your application.
- Image Scanning: Regularly scan your Docker images for vulnerabilities using ECR’s built-in scanning or third-party tools.
- Resource Limits: Always define CPU and memory requests and limits for your pods to prevent resource starvation and ensure stability.
By combining these advanced Kubernetes patterns with AWS managed services, you can build a highly available, scalable, and resilient Laravel deployment on EKS.