Orchestrating Microservices with Kubernetes: A Deep Dive into PHP-FPM, Laravel Queues, and MySQL Replication on AWS EKS
Kubernetes Deployment Strategy for PHP-FPM Applications
Deploying PHP-FPM applications on Kubernetes requires careful consideration of resource allocation, scaling, and health checks. We’ll leverage AWS EKS for this example, focusing on a robust deployment strategy that ensures high availability and efficient resource utilization. The core of our PHP application will be a Laravel project, which benefits from efficient background job processing via Laravel Queues.
PHP-FPM Configuration for Kubernetes
The PHP-FPM configuration is critical for performance and stability within a containerized environment. We need to tune the process manager settings to align with Kubernetes’ resource management capabilities. The `pm.max_children`, `pm.start_servers`, `pm.min_spare_servers`, and `pm.max_spare_servers` directives are key. A common approach is to set `pm.max_children` based on the container’s memory limit, leaving room for the web server (e.g., Nginx) and the operating system. For a container with 1GB of RAM, a reasonable starting point might be 10-20 children, depending on the average memory footprint of a single PHP request.
Here’s a sample php-fpm.d/www.conf tailored for Kubernetes:
[global] pid = /run/php/php7.4-fpm.pid error_log = /var/log/php/error.log daemonize = no [www] user = www-data group = www-data listen = /run/php/php7.4-fpm.sock listen.owner = www-data listen.group = www-data listen.mode = 0660 ; Process Manager settings pm = dynamic pm.max_children = 20 pm.start_servers = 5 pm.min_spare_servers = 2 pm.max_spare_servers = 10 pm.max_requests = 500 ; Other settings request_terminate_timeout = 60s request_slowlog_timeout = 30s slowlog = /var/log/php/slow.log catch_workers_output = yes access.log = /var/log/php/access.log clear_env = no
The daemonize = no setting is crucial for containerized environments, ensuring PHP-FPM runs in the foreground and its logs are captured by the container runtime. catch_workers_output = yes is also beneficial for debugging within Kubernetes.
Kubernetes Deployment Manifests
We’ll define a Deployment for our PHP-FPM application and a separate Deployment for our Nginx Ingress Controller (or use AWS ALB Ingress Controller). The PHP-FPM Deployment will manage the pods running our application code and PHP-FPM processes. A Service will expose the PHP-FPM pods to the Nginx Ingress Controller.
PHP-FPM Deployment
This manifest defines the PHP-FPM application deployment. It includes resource requests and limits, readiness and liveness probes, and a volume for persistent logs (though often logs are shipped to a centralized logging system).
apiVersion: apps/v1
kind: Deployment
metadata:
name: php-fpm-app
labels:
app: php-fpm
spec:
replicas: 3
selector:
matchLabels:
app: php-fpm
template:
metadata:
labels:
app: php-fpm
spec:
containers:
- name: php-fpm
image: your-dockerhub-username/your-php-fpm-app:latest
ports:
- containerPort: 9000
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "1Gi"
cpu: "500m"
livenessProbe:
exec:
command: ["sh", "-c", "kill -0 `cat /run/php/php7.4-fpm.pid`"]
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
exec:
command: ["sh", "-c", "php-fpm -t"]
initialDelaySeconds: 5
periodSeconds: 5
volumeMounts:
- name: php-logs
mountPath: /var/log/php
volumes:
- name: php-logs
emptyDir: {}
PHP-FPM Service
This Service exposes the PHP-FPM pods internally within the cluster, allowing the Nginx Ingress Controller to forward requests.
apiVersion: v1
kind: Service
metadata:
name: php-fpm-service
spec:
selector:
app: php-fpm
ports:
- protocol: TCP
port: 9000
targetPort: 9000
Nginx Ingress Controller Configuration
Assuming you have an Nginx Ingress Controller deployed (e.g., using the official Helm chart), you’ll create an Ingress resource to route external traffic to your application. This Ingress will point to the php-fpm-service.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: my-laravel-app-ingress
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
# Add other Nginx specific annotations as needed
spec:
rules:
- host: myapp.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: php-fpm-service # This should point to your Nginx service if Nginx is separate
port:
number: 80 # Assuming Nginx listens on port 80
Note: The above Ingress example assumes Nginx is directly serving static assets and proxying to PHP-FPM. A more common pattern is to have a separate Nginx container within the same pod or a dedicated Nginx Deployment that proxies to the PHP-FPM Service. If using AWS ALB Ingress Controller, the annotations and structure will differ.
Laravel Queues and Background Job Orchestration
For background job processing, Laravel Queues are essential. We’ll deploy a separate Kubernetes Deployment for the queue worker. This worker will continuously poll the queue for new jobs and execute them. This decouples long-running tasks from the web request lifecycle, improving application responsiveness.
Queue Worker Deployment
This deployment runs a container that executes the php artisan queue:work command. It’s crucial to configure appropriate resource requests and limits, as queue workers can be resource-intensive.
apiVersion: apps/v1
kind: Deployment
metadata:
name: laravel-queue-worker
labels:
app: queue-worker
spec:
replicas: 2 # Scale based on job load
selector:
matchLabels:
app: queue-worker
template:
metadata:
labels:
app: queue-worker
spec:
containers:
- name: queue-worker
image: your-dockerhub-username/your-laravel-app:latest # Same app image, different entrypoint/command
command: ["php", "artisan", "queue:work", "--tries=3", "--timeout=300"]
resources:
requests:
memory: "512Mi"
cpu: "500m"
limits:
memory: "2Gi"
cpu: "1000m"
env:
- name: QUEUE_CONNECTION
value: "redis" # Or database, sqs, etc.
# Add other necessary environment variables
The command overrides the default container entrypoint to start the queue worker. The --tries and --timeout flags are essential for robust job handling. Scaling the number of replicas for this deployment should be based on the observed load on your queues.
Queue Worker Service (Optional but Recommended)
While not strictly necessary for the worker to function, a Service can be useful for monitoring or exposing metrics if your worker application exposes them.
apiVersion: v1
kind: Service
metadata:
name: laravel-queue-worker-service
spec:
selector:
app: queue-worker
ports:
- protocol: TCP
port: 8080 # Example port for metrics
targetPort: 8080
MySQL Replication on AWS EKS
For database high availability, we’ll configure MySQL replication. This typically involves a primary instance and one or more replica instances. On AWS EKS, you have several options:
- AWS RDS: The simplest and most recommended approach. Provision a Multi-AZ RDS instance for automatic failover and read replicas for scaling read traffic. EKS pods connect to the RDS endpoint.
- Self-Managed MySQL on EKS: Deploy MySQL using StatefulSets and PersistentVolumes. This offers more control but significantly increases operational complexity.
We’ll focus on the self-managed approach for demonstration, as it highlights Kubernetes orchestration capabilities. For production, AWS RDS is generally preferred.
MySQL StatefulSet and Service
A StatefulSet is ideal for databases as it provides stable network identifiers and persistent storage for each pod. We’ll define a primary and a replica configuration.
apiVersion: v1
kind: Service
metadata:
name: mysql-headless
labels:
app: mysql
spec:
ports:
- port: 3306
name: mysql
clusterIP: None # Headless service
selector:
app: mysql
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: mysql
spec:
serviceName: "mysql-headless"
replicas: 2 # 1 primary, 1 replica
selector:
matchLabels:
app: mysql
template:
metadata:
labels:
app: mysql
spec:
containers:
- name: mysql
image: mysql:8.0
ports:
- containerPort: 3306
name: mysql
env:
- name: MYSQL_ROOT_PASSWORD
valueFrom:
secretKeyRef:
name: mysql-secrets
key: root-password
- name: MYSQL_DATABASE
value: "laravel_db"
- name: MYSQL_USER
value: "laravel_user"
- name: MYSQL_PASSWORD
valueFrom:
secretKeyRef:
name: mysql-secrets
key: user-password
volumeMounts:
- name: mysql-persistent-storage
mountPath: /var/lib/mysql
volumeClaimTemplates:
- metadata:
name: mysql-persistent-storage
spec:
accessModes: [ "ReadWriteOnce" ]
resources:
requests:
storage: 10Gi
storageClassName: "gp2" # Or your preferred EBS CSI driver storage class
This setup creates a headless service for stable DNS names (e.g., mysql-0.mysql-headless.default.svc.cluster.local) and a StatefulSet. You’ll need to manually configure replication between the pods after they are up and running, or use a more sophisticated operator like Percona Operator for MySQL.
Configuring MySQL Replication
Manual replication setup involves:
- Initializing the primary MySQL instance with a specific server ID and enabling binary logging.
- Configuring a replication user on the primary.
- On the replica instance, setting its server ID, and then using
CHANGE MASTER TOwith the primary’s details and the replication user credentials. - Starting the replication process.
This process is complex to automate within Kubernetes manifests alone and often requires custom scripts or operators. For instance, you might have an init container that checks if it’s the first pod (primary) or a subsequent pod (replica) and configures itself accordingly.
A simplified approach for demonstration within Kubernetes might involve a single MySQL pod acting as primary and another pod configured as a replica. The application would then be configured to use the primary for writes and potentially a read-only replica Service for reads.
Application Configuration for Database Access
Your Laravel application’s .env file (or environment variables injected via Kubernetes) will need to reflect the database setup. For read/write splitting, you’d define multiple database connections.
DB_CONNECTION=mysql DB_HOST=mysql-0.mysql-headless.default.svc.cluster.local # Primary DB DB_PORT=3306 DB_DATABASE=laravel_db DB_USERNAME=laravel_user DB_PASSWORD=your_user_password DB_READ_HOST=mysql-1.mysql-headless.default.svc.cluster.local # Replica DB DB_READ_PORT=3306 DB_READ_DATABASE=laravel_db DB_READ_USERNAME=laravel_user DB_READ_PASSWORD=your_user_password
In your Laravel application’s config/database.php, you would configure read/write connections:
return [
// ... other configurations
'connections' => [
'mysql' => [
'driver' => 'mysql',
'url' => env('DATABASE_URL'),
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', '3306'),
'database' => env('DB_DATABASE', 'forge'),
'username' => env('DB_USERNAME', 'forge'),
'password' => env('DB_PASSWORD', ''),
// ...
],
'mysql_read' => [
'driver' => 'mysql',
'url' => env('DATABASE_URL'),
'host' => env('DB_READ_HOST', '127.0.0.1'),
'port' => env('DB_READ_PORT', '3306'),
'database' => env('DB_READ_DATABASE', 'forge'),
'username' => env('DB_READ_USERNAME', 'forge'),
'password' => env('DB_READ_PASSWORD', ''),
// ...
],
],
'redis' => [
// ... Redis configuration for queues
],
];
Then, in your Eloquent models, you can specify the connection:
class User extends Model
{
protected $connection = 'mysql_read'; // Use read replica for fetching users
}
class Order extends Model
{
protected $connection = 'mysql'; // Use primary for orders
}
Monitoring and Logging
Effective monitoring and logging are paramount in a microservices architecture. For PHP-FPM, ensure logs are collected. For Laravel Queues, monitor queue lengths and job processing times. For MySQL, track replication lag and performance metrics.
- Logging: Integrate with a centralized logging solution like Elasticsearch/Fluentd/Kibana (EFK) or AWS CloudWatch Logs. Configure your PHP-FPM and application to output logs in a structured format (e.g., JSON).
- Metrics: Deploy Prometheus and Grafana for metrics collection and visualization. Use exporters for PHP-FPM (e.g., pm_exporter), MySQL, and potentially custom application metrics.
- Alerting: Configure Alertmanager to notify on critical events, such as high replication lag, failing queue workers, or PHP error rates exceeding thresholds.
Conclusion and Next Steps
This deep dive provides a foundational Kubernetes deployment strategy for PHP-FPM and Laravel applications on AWS EKS, incorporating essential components like queue workers and MySQL replication. Key takeaways include the importance of fine-tuning PHP-FPM settings for containers, defining robust Kubernetes manifests with health checks, and understanding the complexities of database orchestration. For production environments, leveraging managed services like AWS RDS and considering Kubernetes Operators for stateful applications will significantly reduce operational overhead and enhance reliability.