Orchestrating Microservices with Kubernetes: Advanced Strategies for PHP and Laravel Applications on AWS
Leveraging AWS EKS for PHP Microservices: A Deep Dive
Orchestrating complex PHP microservices, especially within a Laravel ecosystem, demands a robust and scalable platform. Amazon Elastic Kubernetes Service (EKS) provides a managed Kubernetes experience on AWS, abstracting away the complexities of control plane management. This allows development and operations teams to focus on deploying and managing their containerized applications. This post will explore advanced strategies for deploying and managing PHP/Laravel microservices on EKS, focusing on production-readiness, scalability, and observability.
Containerizing Laravel Applications for EKS
The foundation of any Kubernetes deployment is a well-crafted container image. For Laravel applications, this involves a multi-stage Dockerfile to keep the final image lean and secure. We’ll include essential build tools and production dependencies only in the final stage.
Consider the following multi-stage Dockerfile:
# Stage 1: Builder
FROM php:8.2-fpm-alpine AS builder
# Install system dependencies for Composer and PHP extensions
RUN apk add --no-cache \
git \
zip \
unzip \
icu-dev \
libzip-dev \
libpng-dev \
jpeg-dev \
freetype-dev \
libjpeg-turbo-dev \
&& docker-php-ext-configure gd --with-freetype --with-jpeg \
&& docker-php-ext-install -j$(nproc) gd \
&& docker-php-ext-install -j$(nproc) opcache \
&& docker-php-ext-install -j$(nproc) pdo \
&& docker-php-ext-install -j$(nproc) pdo_mysql \
&& apk del icu-dev libzip-dev libpng-dev jpeg-dev freetype-dev libjpeg-turbo-dev
# Install Composer
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
# Set working directory
WORKDIR /app
# Copy composer.json and composer.lock
COPY composer.json composer.lock ./
# Install dependencies
RUN composer install --no-dev --optimize-autoloader --no-interaction
# Copy application code
COPY . .
# Clear cache and optimize
RUN php artisan optimize:clear \
&& php artisan config:cache \
&& php artisan route:cache \
&& php artisan view:cache
# Stage 2: Production
FROM php:8.2-fpm-alpine
# Install runtime dependencies
RUN apk add --no-cache \
icu-dev \
libzip-dev \
libpng-dev \
jpeg-dev \
freetype-dev \
libjpeg-turbo-dev \
&& docker-php-ext-configure gd --with-freetype --with-jpeg \
&& docker-php-ext-install -j$(nproc) gd \
&& docker-php-ext-install -j$(nproc) opcache \
&& docker-php-ext-install -j$(nproc) pdo \
&& docker-php-ext-install -j$(nproc) pdo_mysql \
&& apk del icu-dev libzip-dev libpng-dev jpeg-dev freetype-dev libjpeg-turbo-dev
# Copy application code and dependencies from builder stage
COPY --from=builder /app /app
# Set working directory
WORKDIR /app
# Expose port
EXPOSE 9000
# Set user for running the application (optional but recommended for security)
# RUN chown -R www-data:www-data /app
# USER www-data
# Default command to run PHP-FPM
CMD ["php-fpm"]
Kubernetes Manifests for Laravel Microservices
Deploying a Laravel microservice on EKS involves several Kubernetes objects: Deployment, Service, and potentially Ingress. We’ll define these using YAML manifests.
Deployment Manifest
The Deployment object manages the desired state of your application pods. It ensures that a specified number of replicas are running and handles rolling updates.
apiVersion: apps/v1
kind: Deployment
metadata:
name: laravel-user-service
labels:
app: laravel-user-service
spec:
replicas: 3 # Start with 3 replicas for high availability
selector:
matchLabels:
app: laravel-user-service
template:
metadata:
labels:
app: laravel-user-service
spec:
containers:
- name: laravel-user-service
image: <your-ecr-repo-url>/laravel-user-service:latest # Replace with your ECR image
ports:
- containerPort: 9000 # Port PHP-FPM is listening on
env:
- name: APP_ENV
value: "production"
- name: APP_KEY
valueFrom:
secretKeyRef:
name: laravel-secrets
key: APP_KEY
- name: DB_HOST
valueFrom:
secretKeyRef:
name: laravel-secrets
key: DB_HOST
- name: DB_PORT
valueFrom:
secretKeyRef:
name: laravel-secrets
key: DB_PORT
- name: DB_DATABASE
valueFrom:
secretKeyRef:
name: laravel-secrets
key: DB_DATABASE
- name: DB_USERNAME
valueFrom:
secretKeyRef:
name: laravel-secrets
key: DB_USERNAME
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: laravel-secrets
key: DB_PASSWORD
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "500m"
memory: "512Mi"
livenessProbe:
httpGet:
path: /healthz # Assuming you have a health check endpoint
port: 9000
initialDelaySeconds: 15
periodSeconds: 20
readinessProbe:
httpGet:
path: /readyz # Assuming you have a readiness endpoint
port: 9000
initialDelaySeconds: 5
periodSeconds: 10
# Optional: Define resource requests and limits for better scheduling and stability
# Optional: Define affinity rules for pod placement (e.g., anti-affinity to spread pods across nodes)
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchLabels:
app: laravel-user-service
topologyKey: "kubernetes.io/hostname"
Service Manifest
A Service provides a stable IP address and DNS name for a set of pods, enabling inter-service communication and external access.
apiVersion: v1
kind: Service
metadata:
name: laravel-user-service
spec:
selector:
app: laravel-user-service
ports:
- protocol: TCP
port: 80 # The port the service will be exposed on
targetPort: 9000 # The port your container is listening on
type: ClusterIP # Default type, accessible within the cluster
Ingress Manifest (for external access)
If your microservice needs to be accessible from outside the Kubernetes cluster, you’ll use an Ingress resource. This typically requires an Ingress Controller (like AWS Load Balancer Controller) to be installed on your EKS cluster.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: laravel-user-service-ingress
annotations:
# AWS Load Balancer Controller 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" # Replace with your ACM certificate ARN
alb.ingress.kubernetes.io/ssl-redirect: "true"
spec:
rules:
- http:
paths:
- path: /users # Or a more specific path for this service
pathType: Prefix
backend:
service:
name: laravel-user-service
port:
number: 80
Managing Secrets with AWS Secrets Manager and CSI Driver
Storing sensitive information like API keys, database credentials, and JWT secrets directly in Kubernetes manifests or environment variables is a security risk. AWS Secrets Manager is an excellent choice for managing these secrets. The Kubernetes Secrets Store CSI Driver allows EKS pods to mount secrets from AWS Secrets Manager directly as volumes.
Steps to Integrate AWS Secrets Manager with EKS:
- Create a Secret in AWS Secrets Manager: Store your Laravel application’s secrets (e.g.,
APP_KEY, database credentials) in AWS Secrets Manager. - Configure IAM Permissions: Ensure your EKS worker nodes have an IAM role with permissions to access AWS Secrets Manager (e.g.,
secretsmanager:GetSecretValue). - Install the Secrets Store CSI Driver: Deploy the Secrets Store CSI Driver to your EKS cluster. This typically involves applying a set of Kubernetes manifests.
- Install the AWS Provider for Secrets Store CSI Driver: Deploy the AWS provider for the CSI driver.
- Create a `SecretProviderClass` resource: This Kubernetes resource tells the CSI driver how to fetch secrets from AWS Secrets Manager.
apiVersion: secrets-store.csi.x-k8s.io/v1
kind: SecretProviderClass
metadata:
name: aws-secrets-provider
namespace: default # Or the namespace of your application
spec:
provider: aws
parameters:
objects: |
- objectName: "arn:aws:secretsmanager:us-east-1:123456789012:secret:laravel-secrets-abcdef" # Replace with your secret ARN
objectType: "secretsmanager"
jmesPath:
- path: "APP_KEY"
objectAlias: "APP_KEY"
- path: "DB_HOST"
objectAlias: "DB_HOST"
- path: "DB_PORT"
objectAlias: "DB_PORT"
- path: "DB_DATABASE"
objectAlias: "DB_DATABASE"
- path: "DB_USERNAME"
objectAlias: "DB_USERNAME"
- path: "DB_PASSWORD"
objectAlias: "DB_PASSWORD"
Then, modify your Deployment manifest to mount the secrets as a volume:
apiVersion: apps/v1
kind: Deployment
metadata:
name: laravel-user-service
labels:
app: laravel-user-service
spec:
replicas: 3
selector:
matchLabels:
app: laravel-user-service
template:
metadata:
labels:
app: laravel-user-service
spec:
containers:
- name: laravel-user-service
image: <your-ecr-repo-url>/laravel-user-service:latest
ports:
- containerPort: 9000
volumeMounts:
- name: secrets-store-inline
mountPath: "/mnt/secrets-store"
readOnly: true
env:
- name: APP_ENV
value: "production"
# These env vars will now be populated from the mounted secrets
- name: APP_KEY
valueFrom:
secretKeyRef:
name: secrets-store-inline # Name of the volume
key: APP_KEY
- name: DB_HOST
valueFrom:
secretKeyRef:
name: secrets-store-inline
key: DB_HOST
# ... other DB credentials ...
volumes:
- name: secrets-store-inline
csi:
driver: secrets-store.csi.k8s.io
readOnly: true
volumeAttributes:
secretProviderClass: "aws-secrets-provider" # Matches the SecretProviderClass name
Database Connectivity for Microservices
For database connectivity, especially with RDS on AWS, consider using the RDS Proxy. It provides a highly available, fully managed database proxy that makes applications more scalable and resilient to database failures. When using EKS, you can configure your Laravel application to connect to the RDS Proxy endpoint instead of directly to the RDS instance.
Connecting to RDS via RDS Proxy:
- Create an RDS Proxy: In the AWS console, create an RDS Proxy for your existing RDS instance.
- Configure IAM Authentication (Recommended): For enhanced security, configure IAM database authentication for your RDS instance and RDS Proxy. This allows your EKS pods to authenticate using IAM roles instead of database passwords.
- Update Laravel Database Configuration: In your Laravel application’s
.envfile (or preferably, via environment variables injected from Kubernetes secrets), update the database connection details to point to the RDS Proxy endpoint.
# Example .env configuration (values injected via Kubernetes Secrets) DB_CONNECTION=mysql DB_HOST=your-rds-proxy-endpoint.rds.amazonaws.com # Replace with your RDS Proxy endpoint DB_PORT=3306 DB_DATABASE=your_database_name DB_USERNAME=your_iam_user_or_db_user DB_PASSWORD= # If using IAM auth, this might be empty or a placeholder
When using IAM authentication with RDS Proxy, ensure the IAM role associated with your EKS worker nodes (or the Service Account used by your pods) has the necessary IAM policies to authenticate with RDS. The `rds-db:connect` action is crucial here.
Observability: Logging, Metrics, and Tracing
Effective observability is paramount for microservices. For PHP/Laravel applications on EKS, a comprehensive strategy involves:
Logging: Fluentd/Fluent Bit and Elasticsearch/OpenSearch
Collect logs from your application pods and forward them to a centralized logging system. A common pattern is to deploy Fluentd or Fluent Bit as a DaemonSet on EKS to collect logs from all nodes. These logs can then be forwarded to Amazon OpenSearch Service (or Elasticsearch) for indexing and analysis.
# Example Fluent Bit DaemonSet configuration snippet
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: fluent-bit
namespace: kube-system
spec:
template:
spec:
containers:
- name: fluent-bit
image: fluent/fluent-bit:latest
# ... volume mounts for log collection ...
env:
- name: OUTPUT_ES_HOST
value: "your-opensearch-domain.region.es.amazonaws.com"
- name: OUTPUT_ES_PORT
value: "443"
- name: OUTPUT_ES_REGION
value: "us-east-1"
- name: OUTPUT_ES_USE_HTTP
value: "false" # For HTTPS
- name: OUTPUT_ES_RETRY_LIMIT
value: "10"
# ... other configuration ...
Metrics: Prometheus and Grafana
Deploy Prometheus to scrape metrics from your applications and Kubernetes components. You can expose application-level metrics from your Laravel app using libraries like Prometheus client for PHP. Grafana can then be used to visualize these metrics.
// Example using prometheus_client_php in Laravel
use Prometheus\CollectorRegistry;
use Prometheus\Render\RenderTextFormat;
use Prometheus\Storage\InMemory;
// ... in a controller or middleware ...
$registry = new CollectorRegistry(new InMemory());
$counter = $registry->registerCounter('http_requests_total', 'Total HTTP requests', ['method', 'path']);
$counter->incBy(1, ['GET', '/users']);
// To expose metrics endpoint (e.g., /metrics)
if ($request->path() === 'metrics') {
$renderer = new RenderTextFormat();
return response($renderer->render($registry->getMetricFamilySamples()), 200)
->header('Content-Type', RenderTextFormat::MIME_TYPE);
}
Configure Prometheus to scrape your application pods by annotating your Service or Deployment with appropriate scraping configurations.
Distributed Tracing: Jaeger or AWS X-Ray
Implement distributed tracing to understand request flows across your microservices. Libraries like OpenTelemetry can be integrated into your Laravel applications. For AWS integration, AWS X-Ray provides a managed tracing solution. You can deploy the X-Ray daemon as a sidecar container or as a DaemonSet.
CI/CD Pipeline for EKS Deployments
Automating deployments to EKS is crucial for agility. A typical CI/CD pipeline using AWS services might look like this:
- CodeCommit/GitHub: Source code repository.
- AWS CodeBuild: Builds the Docker image, pushes it to Amazon ECR.
- AWS CodePipeline: Orchestrates the build and deployment process.
- AWS CodeDeploy (optional for Kubernetes): Can be used for more advanced deployment strategies, but often Kubernetes native tools like
kubectl applyor Helm are sufficient. - Kubernetes Manifests/Helm Charts: Stored in a repository (e.g., S3, GitHub) and applied to EKS.
A simplified deployment step in CodePipeline might involve invoking a Lambda function or a CodeBuild project that executes kubectl apply -f manifests/ or a Helm upgrade command.
Advanced Considerations: Autoscaling and Service Mesh
Horizontal Pod Autoscaler (HPA)
Configure HPA to automatically scale the number of pods in your Deployment based on observed CPU utilization or custom metrics. Ensure your pods have appropriate resource requests and limits defined.
apiVersion: autoscaling/v1
kind: HorizontalPodAutoscaler
metadata:
name: laravel-user-service-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: laravel-user-service
minReplicas: 3
maxReplicas: 10
targetCPUUtilizationPercentage: 70 # Scale up when CPU exceeds 70%
Cluster Autoscaler
To scale the underlying EKS nodes, deploy the Kubernetes Cluster Autoscaler. This component automatically adjusts the number of nodes in your EKS cluster based on pending pods that cannot be scheduled due to resource constraints.
Service Mesh (Istio/Linkerd)
For more complex microservice architectures, consider adopting a service mesh like Istio or Linkerd. They provide advanced capabilities such as:
- Traffic management (e.g., canary deployments, A/B testing)
- Enhanced security (e.g., mutual TLS)
- Advanced observability (e.g., detailed request metrics, distributed tracing integration)
- Resiliency patterns (e.g., circuit breakers, retries)
Integrating a service mesh adds complexity but offers significant benefits for managing distributed systems at scale.