Beyond Containers: Mastering Kubernetes for High-Availability Laravel Deployments on AWS EKS
Leveraging AWS EKS for Resilient Laravel Applications
While containerization with Docker and orchestration with Kubernetes have become standard practice, achieving true high availability for stateful applications like Laravel, especially when dealing with databases and persistent storage, requires a deeper dive into the nuances of cloud-native deployments. This post outlines a robust architecture for deploying Laravel applications on AWS Elastic Kubernetes Service (EKS), focusing on resilience, scalability, and operational efficiency. We’ll move beyond basic container deployment to address critical aspects like database management, persistent storage, and traffic management.
Database Strategy: RDS with Aurora Serverless or Self-Managed on EKS
The database is often the single point of failure in web applications. For Laravel on EKS, the primary decision revolves around managed database services versus self-hosting within Kubernetes. AWS Relational Database Service (RDS) with Amazon Aurora Serverless offers a compelling managed solution, abstracting away much of the operational overhead. However, for maximum control or specific compliance needs, self-managing a database cluster (e.g., PostgreSQL or MySQL) within EKS using StatefulSets and operators like the Percona Operator for MySQL or Zalando’s Postgres Operator is also viable.
Option 1: AWS RDS with Aurora Serverless
This approach prioritizes operational simplicity and leverages AWS’s managed services for high availability and scalability. Aurora Serverless automatically scales compute and storage, and its multi-AZ deployment provides inherent fault tolerance.
Configuration Steps:
- Provision an Aurora Serverless cluster in your desired AWS region. Configure appropriate instance classes (e.g.,
db.r6g.largefor compute) and storage capacity. - Ensure the Aurora cluster is deployed within the same VPC as your EKS cluster.
- Configure security groups to allow inbound traffic from your EKS worker nodes on the database port (e.g., 3306 for MySQL/Aurora MySQL, 5432 for PostgreSQL/Aurora PostgreSQL).
- Create a dedicated database user and grant necessary privileges for your Laravel application.
Kubernetes Secret for Database Credentials:
Store your database credentials securely using Kubernetes Secrets. This prevents hardcoding sensitive information directly into your deployment manifests.
apiVersion: v1 kind: Secret metadata: name: laravel-db-credentials namespace: default # Or your application's namespace type: Opaque data: DB_HOST: [base64-encoded-aurora-endpoint] # e.g., aabbccddeeff.cluster-xxxxxxxxxxxx.us-east-1.rds.amazonaws.com DB_PORT: [base64-encoded-port] # e.g., MzM w= for 3306 DB_DATABASE: [base64-encoded-db-name] # e.g., laravel_prod DB_USERNAME: [base64-encoded-db-user] # e.g., laravel_user DB_PASSWORD: [base64-encoded-db-password]
You can generate base64 encoded values using the echo -n 'your_value' | base64 command.
Option 2: Self-Managed Database on EKS
This option provides granular control but significantly increases operational complexity. We’ll use a StatefulSet for stable network identities and persistent storage, and a database operator for managing the cluster lifecycle.
Example using Percona Operator for MySQL:
First, install the Percona Operator for MySQL. Refer to the official Percona documentation for the latest installation instructions.
kubectl apply -f https://raw.githubusercontent.com/percona/percona-everest-operator/main/deploy/bundle.yaml
Next, define a PerconaXtraDBCluster custom resource. This will provision a highly available MySQL cluster with replication and automatic failover.
apiVersion: pxc.percona.com/v1
kind: PerconaXtraDBCluster
metadata:
name: laravel-mysql-cluster
namespace: default # Or your application's namespace
spec:
secretsName: mysql-secrets # Kubernetes secret for root password
# ... other configuration like pxc, proxysql, backup, etc.
pxc:
size: 3 # Number of PXC nodes for HA
image: percona/percona-xtradb-cluster:8.0
resources:
requests:
memory: "2Gi"
cpu: "1"
limits:
memory: "4Gi"
cpu: "2"
volumeSpec:
persistentVolumeClaim:
accessModes: [ "ReadWriteOnce" ]
resources:
requests:
storage: 100Gi # Adjust storage as needed
proxysql:
enabled: true
size: 2 # Number of ProxySQL nodes for load balancing
image: percona/percona-xtradb-cluster-operator:1.15.0 # Use appropriate image
resources:
requests:
memory: "512Mi"
cpu: "500m"
limits:
memory: "1Gi"
cpu: "1"
backup:
enabled: true
image: percona/percona-xtradb-cluster-operator:1.15.0 # Use appropriate image
schedule:
- name: "daily"
cron: "0 0 * * *"
keep: 7
storageName: "s3-backup" # Reference to a Storage resource
# ... other configurations for affinity, tolerations, etc.
You’ll also need to configure a Kubernetes Secret for the root password and potentially a StorageClass for your PersistentVolumes. The operator will create a Service for ProxySQL, which your Laravel application will connect to.
Laravel Database Configuration (using ProxySQL Service):
return [
// ... other configurations
'connections' => [
'mysql' => [
'driver' => 'mysql',
'url' => env('DATABASE_URL'),
'host' => env('DB_HOST', 'laravel-mysql-cluster-proxysql.default.svc.cluster.local'), // ProxySQL service name
'port' => env('DB_PORT', 6033), // ProxySQL port
'database' => env('DB_DATABASE', 'laravel_prod'),
'username' => env('DB_USERNAME', 'app_user'),
'password' => env('DB_PASSWORD', ''),
'unix_socket' => env('DB_SOCKET', ''),
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
'prefix' => '',
'prefix_indexes' => true,
'strict' => true,
'engine' => null,
'options' => extension_loaded('pdo_mysql') ? array_filter([
PDO::MYSQL_ATTR_SSL_KEY => env('MYSQL_ATTR_SSL_KEY'),
PDO::MYSQL_ATTR_SSL_CERT => env('MYSQL_ATTR_SSL_CERT'),
PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT => !env('APP_ENV', 'production'),
]) : [],
],
],
// ...
];
Persistent Storage for Laravel (if needed)
Laravel applications might require persistent storage for uploads, logs, or other file-based data. On EKS, this is typically managed using PersistentVolumes (PVs) and PersistentVolumeClaims (PVCs). AWS Elastic File System (EFS) is a popular choice for shared, scalable file storage accessible by multiple pods.
Using AWS EFS with EKS
To integrate EFS, you’ll need the AWS EFS CSI driver. Install it on your EKS cluster.
kubectl apply -k "github.com/aws/eks-charts/stable/aws-efs-csi-driver//crds?ref=master" kubectl apply -k "github.com/aws/eks-charts/stable/aws-efs-csi-driver//incluster?ref=master"
Then, create an StorageClass that references the EFS CSI driver.
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: efs-sc
annotations:
# If you want to make this the default StorageClass for PVCs
# storageclass.kubernetes.io/is-default-class: "true"
provisioner: efs.csi.aws.com
parameters:
provisionerName: efs.csi.aws.com
# Optional: Specify EFS file system ID if you have an existing EFS
# efsFileSystemId: fs-0123456789abcdef0
# Optional: Specify the AWS region where the EFS is located
# region: us-east-1
# Optional: Specify the EFS throughput mode (burst or provisioned)
# throughputMode: provisioned
# Optional: Specify the EFS provisioned throughput in MiB/s (if throughputMode is provisioned)
# provisionedThroughputInMiBps: 100
reclaimPolicy: Retain # Or Delete, depending on your needs
Finally, create a PersistentVolumeClaim in your Laravel application’s deployment to request storage from this StorageClass.
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: laravel-storage
namespace: default # Or your application's namespace
spec:
accessModes:
- ReadWriteMany # EFS supports ReadWriteMany
storageClassName: efs-sc
resources:
requests:
storage: 100Gi # Request desired storage size
Mount this PVC in your Laravel application’s Deployment or StatefulSet definition.
apiVersion: apps/v1
kind: Deployment
metadata:
name: laravel-app
namespace: default
spec:
replicas: 3
selector:
matchLabels:
app: laravel
template:
metadata:
labels:
app: laravel
spec:
containers:
- name: laravel
image: your-laravel-docker-image:latest
ports:
- containerPort: 80
volumeMounts:
- name: laravel-storage-volume
mountPath: /var/www/html/storage # Or your application's storage path
volumes:
- name: laravel-storage-volume
persistentVolumeClaim:
claimName: laravel-storage
Ingress and Load Balancing with AWS Load Balancer Controller
For external access to your Laravel application, the AWS Load Balancer Controller is the recommended approach. It provisions and manages AWS Application Load Balancers (ALBs) or Network Load Balancers (NLBs) based on Kubernetes Ingress resources.
Installing and Configuring the AWS Load Balancer Controller
Follow the official AWS documentation to install the controller. This typically involves creating an IAM OIDC provider for your EKS cluster and an IAM policy for the controller’s service account.
# Example: Create IAM policy (replace with actual ARN from AWS docs) aws iam create-policy --policy-name AWSLoadBalancerControllerIAMPolicy --policy-document file://iam_policy.json # Example: Create Service Account and associate IAM role (refer to AWS docs for detailed steps) # ...
Once installed, you can define an Ingress resource to expose your Laravel application.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: laravel-ingress
namespace: default # Or your application's namespace
annotations:
kubernetes.io/ingress.class: alb
alb.ingress.kubernetes.io/scheme: internet-facing
alb.ingress.kubernetes.io/target-type: ip
# For HTTPS:
# 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: 'true'
spec:
rules:
- http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: laravel-service # The Kubernetes Service exposing your Laravel pods
port:
number: 80 # The port your Laravel application listens on
The AWS Load Balancer Controller will automatically provision an ALB, configure listeners (HTTP/HTTPS), target groups, and health checks based on this Ingress resource. The ALB’s DNS name will be available via kubectl get ingress laravel-ingress -o jsonpath='{.status.loadBalancer.ingress[0].hostname}'.
High Availability Considerations for Laravel Components
Beyond the infrastructure, ensure your Laravel application itself is designed for resilience.
Queues and Background Jobs
Use a robust queue driver like Redis or Amazon SQS. For SQS, you can deploy the AWS SQS Kubernetes Operator or manage SQS queues directly via AWS SDKs within your application or a dedicated job runner pod.
// config/queue.php
'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('AWS_SQS_QUEUE_URL'),
'after_commit' => false,
],
// ...
],
Deploy dedicated worker pods using a Deployment or StatefulSet configured to run php artisan queue:work. Ensure sufficient replicas for your workers.
apiVersion: apps/v1
kind: Deployment
metadata:
name: laravel-queue-worker
namespace: default
spec:
replicas: 3 # Scale based on workload
selector:
matchLabels:
app: laravel-worker
template:
metadata:
labels:
app: laravel-worker
spec:
containers:
- name: worker
image: your-laravel-docker-image:latest
command: ["php", "artisan", "queue:work", "--tries=3", "--timeout=60"] # Adjust as needed
envFrom:
- secretRef:
name: laravel-app-env # Assuming your app env vars are in a secret
resources:
requests:
cpu: "500m"
memory: "1Gi"
limits:
cpu: "1"
memory: "2Gi"
Caching
Utilize Redis for caching. Deploy a managed Redis instance (e.g., Amazon ElastiCache for Redis) or use a Redis operator within EKS. Ensure your Redis deployment is also highly available.
// config/cache.php
'stores' => [
// ...
'redis' => [
'driver' => 'redis',
'connection' => 'cache',
],
// ...
],
'default' => env('CACHE_DRIVER', 'redis'),
// config/database.php (for Redis cache connection)
'redis' => [
'driver' => 'redis',
'url' => env('REDIS_URL'),
'host' => env('REDIS_HOST', 'your-redis-host.xxxxxx.ng.0001.use1.cache.amazonaws.com'),
'password' => env('REDIS_PASSWORD', null),
'port' => env('REDIS_PORT', 6379),
'database' => env('REDIS_DB', 0),
],
Monitoring and Logging
Implement comprehensive monitoring and logging. Consider deploying Prometheus and Grafana for metrics, and Fluentd or Fluent Bit for log aggregation, forwarding logs to Amazon CloudWatch Logs or a centralized logging solution.
Example: Fluent Bit for Log Forwarding to CloudWatch
Deploy Fluent Bit as a DaemonSet to collect logs from all nodes and forward them to CloudWatch Logs.
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: fluent-bit
namespace: kube-system # Or a dedicated logging namespace
labels:
k8s-app: fluent-bit
spec:
selector:
matchLabels:
k8s-app: fluent-bit
template:
metadata:
labels:
k8s-app: fluent-bit
spec:
containers:
- name: fluent-bit
image: fluent/fluent-bit:latest # Use a specific, stable version
ports:
- containerPort: 2020 # For HTTP input plugin
volumeMounts:
- name: varlog
mountPath: /var/log
- name: varlibdockercontainers
mountPath: /var/lib/docker/containers
readOnly: true
- name: fluent-bit-config
mountPath: /fluent-bit/etc/
volumes:
- name: varlog
hostPath:
path: /var/log
- name: varlibdockercontainers
hostPath:
path: /var/lib/docker/containers
# Mount the configuration file
- name: fluent-bit-config
configMap:
name: fluent-bit-configmap
items:
- key: fluent-bit.conf
path: fluent-bit.conf
---
apiVersion: v1
kind: ConfigMap
metadata:
name: fluent-bit-configmap
namespace: kube-system # Must match DaemonSet namespace
data:
fluent-bit.conf: |
[SERVICE]
Flush 5
Daemon On
Log_Level info
Parsers on
HTTP_Server On
HTTP_Listen 0.0.0.0
HTTP_Port 2020
@INCLUDE parsers.conf
[INPUT]
Name tail
Path /var/log/containers/*.log
Parser docker
Tag kube.*
Mem_Buf_Limit 5MB
Skip_Long_Lines On
Refresh_Interval 10
[OUTPUT]
Name cloudwatch_logs
Match kube.*
region us-east-1 # Your AWS region
log_group_name /aws/eks/your-cluster-name/containers # Your CloudWatch Log Group
log_stream_prefix ${HOSTNAME}-
auto_create_group True
# IAM role for Fluent Bit to write to CloudWatch Logs
# Ensure the Service Account associated with this pod has the necessary IAM permissions
# (e.g., logs:CreateLogGroup, logs:CreateLogStream, logs:PutLogEvents)
Ensure the IAM role associated with the Fluent Bit service account has the necessary permissions to write to CloudWatch Logs.
Conclusion
Deploying a highly available Laravel application on EKS involves careful consideration of database strategy, persistent storage, ingress, and application-level resilience patterns. By leveraging managed AWS services like RDS and ALB, combined with Kubernetes best practices for StatefulSets, Deployments, and operators, you can build a robust and scalable platform. Continuous monitoring and logging are paramount for maintaining operational health and quickly diagnosing issues in a distributed environment.