Beyond the Basics: Mastering Kubernetes Orchestration for High-Availability Laravel Deployments
Kubernetes Cluster Setup for Laravel HA
Achieving high availability for a Laravel application on Kubernetes necessitates a robust cluster configuration. This involves not just deploying your application but also ensuring its dependencies – databases, caches, and message queues – are equally resilient. We’ll focus on a multi-node setup with at least three worker nodes for redundancy. For simplicity, we’ll assume a managed Kubernetes service (like GKE, EKS, or AKS) or a self-hosted cluster provisioned with tools like `kubeadm`. The core principles remain consistent.
StatefulSets for Database and Cache
For stateful components like your primary database (e.g., PostgreSQL, MySQL) and Redis cache, StatefulSets are the idiomatic Kubernetes workload. They provide stable network identifiers, persistent storage, and ordered, graceful deployment and scaling. This is crucial for maintaining data integrity and availability during rolling updates or node failures.
PostgreSQL with Replication
A common pattern for HA databases is a primary-replica setup. We’ll define a StatefulSet for the PostgreSQL instances and a Service of type ClusterIP that targets the primary. A separate Service can target all replicas for read-only traffic, or you can manage failover logic within your application or a dedicated operator.
PostgreSQL StatefulSet Definition
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: postgresql
labels:
app: postgresql
spec:
serviceName: postgresql-headless # Headless service for stable DNS
replicas: 3 # One primary, two replicas
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
value: "laravel_user"
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: postgresql-secret
key: password
- name: POSTGRES_DB
value: "laravel_db"
volumeMounts:
- name: postgresql-persistent-storage
mountPath: /var/lib/postgresql/data
readinessProbe:
exec:
command: ["pg_isready", "-U", "laravel_user"]
initialDelaySeconds: 15
periodSeconds: 10
livenessProbe:
exec:
command: ["pg_isready", "-U", "laravel_user"]
initialDelaySeconds: 30
periodSeconds: 20
volumeClaimTemplates:
- metadata:
name: postgresql-persistent-storage
spec:
accessModes: [ "ReadWriteOnce" ]
resources:
requests:
storage: 10Gi
storageClassName: "your-storage-class" # e.g., gp2, standard, etc.
PostgreSQL Headless Service
apiVersion: v1
kind: Service
metadata:
name: postgresql-headless
labels:
app: postgresql
spec:
ports:
- port: 5432
targetPort: 5432
clusterIP: None # Headless service
selector:
app: postgresql
PostgreSQL Primary Service
apiVersion: v1
kind: Service
metadata:
name: postgresql-primary
labels:
app: postgresql
spec:
ports:
- port: 5432
targetPort: 5432
selector:
app: postgresql # This will target the primary pod based on its ordinal index
Note on Replication and Failover: The above definition sets up the pods and persistent storage. Actual PostgreSQL replication (streaming replication) and automatic failover require additional configuration. This can be achieved using tools like Patroni, Stolon, or by leveraging a Kubernetes operator specifically designed for PostgreSQL (e.g., Crunchy Data PostgreSQL Operator, Zalando PostgreSQL Operator). For a production HA setup, integrating one of these is highly recommended. The postgresql-primary service would then be configured to point to the current primary instance, often managed by the operator.
Redis Cluster for Caching and Queues
For Redis, a StatefulSet can also be used, especially for clustered deployments or when stable network identities are paramount. For simpler caching needs, a single instance with a Deployment and Service might suffice, but for HA, a cluster or sentinel setup is preferred. We’ll outline a basic Redis cluster setup.
Redis Cluster StatefulSet Definition
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: redis-cluster
labels:
app: redis-cluster
spec:
serviceName: redis-cluster-headless
replicas: 6 # Minimum for Redis Cluster is 6 nodes (3 masters, 3 replicas)
selector:
matchLabels:
app: redis-cluster
template:
metadata:
labels:
app: redis-cluster
spec:
containers:
- name: redis
image: redis:7-alpine
command: ["redis-server", "/usr/local/etc/redis/redis.conf"]
ports:
- containerPort: 6379
name: tcp-redis
- containerPort: 16379 # Cluster bus port
name: tcp-cluster
volumeMounts:
- name: redis-config
mountPath: /usr/local/etc/redis/redis.conf
subPath: redis.conf
- name: redis-persistent-storage
mountPath: /data
readinessProbe:
exec:
command: ["redis-cli", "ping"]
initialDelaySeconds: 5
periodSeconds: 5
livenessProbe:
exec:
command: ["redis-cli", "ping"]
initialDelaySeconds: 15
periodSeconds: 15
volumeClaimTemplates:
- metadata:
name: redis-persistent-storage
spec:
accessModes: [ "ReadWriteOnce" ]
resources:
requests:
storage: 5Gi
storageClassName: "your-storage-class"
---
apiVersion: v1
kind: ConfigMap
metadata:
name: redis-config
data:
redis.conf: |
port 6379
cluster-enabled yes
cluster-config-file /data/nodes.conf
cluster-node-timeout 5000
appendonly yes
appendfilename "appendonly.aof"
dbfilename "dump.rdb"
logfile "/var/log/redis/redis-server.log"
dir "/data"
bind 0.0.0.0
Redis Cluster Headless Service
apiVersion: v1
kind: Service
metadata:
name: redis-cluster-headless
labels:
app: redis-cluster
spec:
ports:
- port: 6379
targetPort: 6379
name: tcp-redis
- port: 16379
targetPort: 16379
name: tcp-cluster
clusterIP: None
selector:
app: redis-cluster
Redis Cluster Initialization Job
apiVersion: batch/v1
kind: Job
metadata:
name: redis-cluster-init
spec:
template:
spec:
containers:
- name: redis-cluster-creator
image: redis:7-alpine
command: ["/bin/sh", "-c"]
args:
- |
set -e
# Wait for all nodes to be ready
echo "Waiting for Redis nodes to start..."
for i in $(seq 0 5); do
while ! redis-cli -h redis-cluster-headless -p 6379 ping; do
sleep 1
done
done
echo "All Redis nodes are up. Creating cluster..."
# Check if cluster is already created
if [ "$(redis-cli -h redis-cluster-headless -p 6379 cluster info | grep cluster_state:ok)" ]; then
echo "Cluster already initialized."
exit 0
fi
# Create cluster
redis-cli --cluster create \
redis-cluster-headless:6379 \
redis-cluster-headless:6379 \
redis-cluster-headless:6379 \
redis-cluster-headless:6379 \
redis-cluster-headless:6379 \
redis-cluster-headless:6379 \
--cluster-replicas 1 \
-a $(grep -oP '(?<=requirepass ).*' /etc/redis/redis.conf | head -n 1) # If password is set
echo "Cluster created."
exit 0
restartPolicy: Never
backoffLimit: 4
Note on Redis Cluster Initialization: The redis-cluster-init Job is crucial. It waits for all Redis pods to be running and then uses redis-cli --cluster create to form the cluster. The --cluster-replicas 1 flag ensures each master has one replica. This job should be run after the StatefulSet and Service are deployed. You might need to adjust the number of replicas and the sequence of commands based on your specific Redis HA strategy (e.g., Sentinel for simpler HA).
Laravel Application Deployment
Your Laravel application will typically be deployed using a Deployment. For HA, you'll want multiple replicas, a Service of type LoadBalancer or NodePort (if behind an external load balancer), and proper health checks.
Laravel Deployment Definition
apiVersion: apps/v1
kind: Deployment
metadata:
name: laravel-app
labels:
app: laravel-app
spec:
replicas: 3 # Adjust based on expected load and desired availability
selector:
matchLabels:
app: laravel-app
template:
metadata:
labels:
app: laravel-app
spec:
containers:
- name: laravel
image: your-docker-registry/your-laravel-app:latest # Replace with your image
ports:
- containerPort: 8000 # Or your application's port
env:
- name: APP_ENV
value: "production"
- name: APP_DEBUG
value: "false"
- name: DB_HOST
value: "postgresql-primary.default.svc.cluster.local" # Service name for primary DB
- name: DB_PORT
value: "5432"
- name: DB_DATABASE
value: "laravel_db"
- name: DB_USERNAME
value: "laravel_user"
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: postgresql-secret
key: password
- name: REDIS_HOST
value: "redis-cluster-headless.default.svc.cluster.local" # Headless service for Redis cluster
- name: REDIS_PORT
value: "6379"
- name: QUEUE_CONNECTION
value: "redis"
readinessProbe:
httpGet:
path: /health # Your application's health check endpoint
port: 8000
initialDelaySeconds: 10
periodSeconds: 5
livenessProbe:
httpGet:
path: /health # Your application's health check endpoint
port: 8000
initialDelaySeconds: 20
periodSeconds: 10
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
# Optional: Define affinity/anti-affinity rules for better distribution
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchExpressions:
- key: app
operator: In
values:
- laravel-app
topologyKey: "kubernetes.io/hostname"
---
apiVersion: v1
kind: Service
metadata:
name: laravel-app-service
labels:
app: laravel-app
spec:
selector:
app: laravel-app
ports:
- protocol: TCP
port: 80
targetPort: 8000
type: LoadBalancer # Or NodePort if using an external LB
Environment Variables and Secrets
Database credentials, API keys, and other sensitive information should be managed using Kubernetes Secrets. Non-sensitive configuration can be managed via ConfigMaps. The Laravel application should be configured to read these values from environment variables.
Example PostgreSQL Secret
apiVersion: v1 kind: Secret metadata: name: postgresql-secret type: Opaque data: password: [base64-encoded-password]
To create this secret, you can use:
echo -n 'your_super_secret_password' | base64 # Then paste the output into the 'data.password' field.
Ingress Controller for External Access
To expose your Laravel application to the internet, an Ingress controller is the standard Kubernetes approach. Popular choices include Nginx Ingress Controller, Traefik, or HAProxy Ingress. This provides features like SSL termination, path-based routing, and load balancing.
Nginx Ingress Controller Deployment (Example)
You'll typically install an Ingress controller using Helm or its provided manifests. Here's a conceptual overview of how you'd configure an Ingress resource to route traffic to your Laravel service.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: laravel-ingress
annotations:
nginx.ingress.kubernetes.io/rewrite-target: / # Example annotation for Nginx
spec:
ingressClassName: nginx # Ensure this matches your Ingress controller's class
rules:
- host: your-laravel-app.com # Your domain name
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: laravel-app-service
port:
number: 80
Advanced Considerations for Production
Resource Management and Autoscaling
Define appropriate requests and limits for CPU and memory in your container specifications. For dynamic scaling, implement HorizontalPodAutoscaler (HPA) based on CPU utilization, memory, or custom metrics. Consider VerticalPodAutoscaler (VPA) for optimizing resource requests/limits over time.
Persistent Storage Strategy
Choose a reliable StorageClass for your persistent volumes. For production, consider solutions that offer features like snapshots, backups, and replication (e.g., cloud provider CSI drivers, Ceph, Longhorn). Ensure your accessModes (e.g., ReadWriteOnce, ReadWriteMany) are appropriate for your workload.
Monitoring and Logging
Integrate a robust monitoring solution (e.g., Prometheus with Grafana) to track application performance, resource usage, and Kubernetes events. Centralized logging (e.g., EFK stack - Elasticsearch, Fluentd, Kibana, or Loki with Promtail) is essential for debugging and auditing.
CI/CD Pipeline Integration
Automate your deployments using a CI/CD pipeline (e.g., GitLab CI, GitHub Actions, Jenkins). Implement strategies like rolling updates, blue-green deployments, or canary releases to minimize downtime during application updates. Use tools like Argo CD or Flux for GitOps-based continuous delivery.
Network Policies
For enhanced security, implement Kubernetes NetworkPolicies to restrict network traffic between pods. For example, only allow your Laravel application pods to communicate with the database and Redis pods on their respective ports.
Example Network Policy
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: laravel-app-network-policy
namespace: default # Ensure this matches your namespace
spec:
podSelector:
matchLabels:
app: laravel-app
policyTypes:
- Ingress
- Egress
ingress:
- from: # Allow ingress from Ingress Controller (if applicable)
- podSelector:
matchLabels:
app.kubernetes.io/name: ingress-nginx # Adjust label based on your ingress controller
egress:
- to:
- podSelector:
matchLabels:
app: postgresql
ports:
- protocol: TCP
port: 5432
- to:
- podSelector:
matchLabels:
app: redis-cluster
ports:
- protocol: TCP
port: 6379
- to: # Allow egress to external services if needed
- ipBlock:
cidr: 0.0.0.0/0
except:
- 10.0.0.0/8 # Example: Exclude private ranges if not needed
- 172.16.0.0/12
- 192.168.0.0/16
ports:
- protocol: TCP
port: 443 # For external API calls over HTTPS
- protocol: UDP
port: 53 # For DNS resolution
This comprehensive approach, combining robust Kubernetes workload definitions with best practices for stateful services, application deployment, and security, forms the foundation for a highly available Laravel deployment.