Orchestrating Microservices with Kubernetes: A Deep Dive into PHP 8/9 and Laravel in a Dockerized AWS Environment
Setting the Stage: Dockerizing PHP 8/9 Laravel Applications
Before we can orchestrate, we must containerize. This section details a robust Docker setup for a typical PHP 8/9 Laravel application, focusing on production-readiness. We’ll leverage multi-stage builds to keep our final image lean and secure.
Consider a standard Dockerfile for our Laravel application:
# Stage 1: Builder
FROM php:8.2-fpm AS builder
# Install essential extensions and tools
RUN apt-get update && apt-get install -y \
git \
unzip \
libzip-dev \
libpng-dev \
libjpeg-dev \
libfreetype6-dev \
libonig-dev \
libxml2-dev \
zip \
acl \
supervisor \
&& rm -rf /var/lib/apt/lists/* \
&& docker-php-ext-configure gd --with-freetype --with-jpeg \
&& docker-php-ext-install -j$(nproc) gd \
&& docker-php-ext-install pdo pdo_mysql zip exif pcntl opcache sockets
# Set working directory
WORKDIR /var/www/html
# Copy composer.json and composer.lock
COPY --chown=www-data:www-data composer.json composer.lock ./
# Install Composer dependencies
RUN composer install --no-dev --no-autoloader --no-scripts --prefer-dist \
&& composer clear-cache
# Copy the rest of the application code
COPY --chown=www-data:www-data . .
# Install Composer autoloader and run post-install scripts
RUN composer dump-autoload --optimize --no-dev
# Permissions for storage and bootstrap/cache
RUN chown -R www-data:www-data storage bootstrap/cache && chmod -R 775 storage bootstrap/cache
# Stage 2: Production Image
FROM php:8.2-fpm-alpine AS production
# Install only necessary extensions for runtime
RUN apk add --no-cache \
libzip \
libpng \
libjpeg-turbo \
freetype \
oniguruma \
libxml2 \
acl \
supervisor \
&& docker-php-ext-configure gd --with-freetype --with-jpeg \
&& docker-php-ext-install -j$(nproc) gd \
&& docker-php-ext-install pdo pdo_mysql zip exif pcntl opcache sockets
# Copy application code from builder stage
COPY --from=builder --chown=www-data:www-data /var/www/html /var/www/html
# Copy optimized autoloader from builder stage
COPY --from=builder --chown=www-data:www-data /var/www/html/vendor /var/www/html/vendor
# Ensure correct permissions
RUN chown -R www-data:www-data storage bootstrap/cache && chmod -R 775 storage bootstrap/cache
# Copy supervisor configuration
COPY docker/supervisor/app.conf /etc/supervisor/conf.d/app.conf
# Expose port
EXPOSE 9000
# Start supervisor
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/supervisord.conf"]
The supervisor configuration (docker/supervisor/app.conf) is crucial for managing the PHP-FPM process and any background queue workers:
[program:php-fpm] process_name=%(program_name)s_%(process_num)02d command=/usr/local/sbin/php-fpm --nodaemonize --fpm-config /usr/local/etc/php-fpm.conf autostart=true autorestart=true user=www-data numprocs=1 redirect_stderr=true stdout_logfile=/var/log/supervisor/php-fpm.log [program:queue-worker] process_name=%(program_name)s_%(process_num)02d command=php artisan queue:work --tries=3 --timeout=60 autostart=true autorestart=true user=www-data numprocs=2 ; Adjust based on expected load redirect_stderr=true stdout_logfile=/var/log/supervisor/queue-worker.log
This setup ensures that PHP-FPM is always running and provides a template for managing queue workers. The multi-stage build significantly reduces the final image size by discarding build dependencies.
Kubernetes Deployment Strategy: Deployments and Services
For Kubernetes, we’ll define Deployments to manage our application pods and Services to expose them internally and externally. We’ll also consider a HorizontalPodAutoscaler for dynamic scaling.
A typical Kubernetes Deployment manifest:
apiVersion: apps/v1
kind: Deployment
metadata:
name: laravel-app-deployment
labels:
app: laravel-app
spec:
replicas: 3 # Initial number of replicas
selector:
matchLabels:
app: laravel-app
template:
metadata:
labels:
app: laravel-app
spec:
containers:
- name: laravel-app
image: YOUR_ECR_REPO/laravel-app:latest # Replace with your ECR image
ports:
- containerPort: 9000
env:
- name: APP_ENV
value: "production"
- name: APP_KEY
valueFrom:
secretKeyRef:
name: laravel-secrets
key: app-key
- name: DB_HOST
value: "mysql-service" # Assuming a MySQL service named mysql-service
- name: DB_PORT
value: "3306"
- name: DB_DATABASE
valueFrom:
secretKeyRef:
name: laravel-secrets
key: db-name
- name: DB_USERNAME
valueFrom:
secretKeyRef:
name: laravel-secrets
key: db-user
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: laravel-secrets
key: db-password
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
livenessProbe:
httpGet:
path: /healthz # A simple health check endpoint in Laravel
port: 9000
initialDelaySeconds: 15
periodSeconds: 20
readinessProbe:
httpGet:
path: /healthz
port: 9000
initialDelaySeconds: 5
periodSeconds: 10
# Optional: Node selectors or affinity rules for specific node placement
# nodeSelector:
# kubernetes.io/os: linux
# affinity:
# podAffinity:
# requiredDuringSchedulingIgnoredDuringExecution:
# - labelSelector:
# matchExpressions:
# - key: app
# operator: In
# values:
# - laravel-app
# topologyKey: "kubernetes.io/hostname"
And a corresponding Service to route traffic to the application pods:
apiVersion: v1
kind: Service
metadata:
name: laravel-app-service
spec:
selector:
app: laravel-app
ports:
- protocol: TCP
port: 80
targetPort: 9000 # The port PHP-FPM is listening on inside the container
type: ClusterIP # Use LoadBalancer or NodePort for external access, or an Ingress controller
For external access, we’ll typically use an Ingress controller (like AWS ALB Ingress Controller or Nginx Ingress Controller). Here’s a basic Ingress resource:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: laravel-app-ingress
annotations:
# Annotations specific to your Ingress controller (e.g., AWS ALB)
kubernetes.io/ingress.class: "alb"
alb.ingress.kubernetes.io/scheme: "internet-facing"
alb.ingress.kubernetes.io/target-type: "ip"
spec:
rules:
- host: your-app.example.com # Your domain name
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: laravel-app-service
port:
number: 80
The HorizontalPodAutoscaler will automatically adjust the number of replicas 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
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70 # Scale up when CPU utilization reaches 70%
Managing Background Jobs: Kubernetes Jobs and CronJobs
Laravel’s queue system is essential for background processing. In Kubernetes, we can manage queue workers using Deployments (as shown above with supervisor) or more granularly with Jobs for one-off tasks and CronJobs for scheduled tasks.
For a persistent queue worker pool managed by Supervisor within the application pod, the previous Deployment configuration is sufficient. However, for specific, long-running or batch jobs, a dedicated Job resource is more appropriate:
apiVersion: batch/v1
kind: Job
metadata:
name: laravel-batch-job
spec:
template:
spec:
containers:
- name: laravel-batch-processor
image: YOUR_ECR_REPO/laravel-app:latest # Use the same application image
command: ["php", "artisan", "my:custom:batch-job", "--env=production"] # Your custom Artisan command
envFrom: # Inherit environment variables from a ConfigMap or Secrets
- configMapRef:
name: laravel-config
- secretRef:
name: laravel-secrets
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "256Mi"
cpu: "200m"
restartPolicy: Never # Or OnFailure
backoffLimit: 4 # Number of retries before marking the job as failed
For scheduled tasks (e.g., daily reports), Kubernetes CronJobs are the standard:
apiVersion: batch/v1
kind: CronJob
metadata:
name: laravel-cron-daily-report
spec:
schedule: "0 2 * * *" # Run daily at 2 AM
jobTemplate:
spec:
template:
spec:
containers:
- name: laravel-cron-reporter
image: YOUR_ECR_REPO/laravel-app:latest
command: ["php", "artisan", "reports:generate:daily", "--env=production"]
envFrom:
- configMapRef:
name: laravel-config
- secretRef:
name: laravel-secrets
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "256Mi"
cpu: "200m"
restartPolicy: OnFailure
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 1
Ensure your Laravel application has the necessary Artisan commands registered (e.g., app/Console/Kernel.php) and that your .env variables are correctly mapped via Kubernetes ConfigMaps and Secrets.
Database and Cache Orchestration with AWS RDS and ElastiCache
For production environments, relying on local databases within containers is not advisable. AWS RDS for MySQL (or PostgreSQL) and ElastiCache for Redis provide managed, scalable, and highly available solutions. These services are accessed directly by your application pods.
When configuring your Laravel application’s .env file (which will be populated via Kubernetes Secrets), the database connection details will point to the AWS service endpoints:
DB_CONNECTION=mysql DB_HOST=your-rds-instance.xxxxxxxxxxxx.us-east-1.rds.amazonaws.com DB_PORT=3306 DB_DATABASE=your_database_name DB_USERNAME=your_db_user DB_PASSWORD=your_db_password REDIS_HOST=your-elasticache-redis-node.xxxxxxxxxxxx.ng.0001.use1.cache.amazonaws.com REDIS_PORT=6379 REDIS_PASSWORD=null # If no password is set
In Kubernetes, these sensitive values should be stored in Secrets. For example, a Secret for database credentials:
apiVersion: v1 kind: Secret metadata: name: laravel-secrets type: Opaque data: app-key: YOUR_BASE64_ENCODED_APP_KEY db-name: YOUR_BASE64_ENCODED_DB_NAME db-user: YOUR_BASE64_ENCODED_DB_USER db-password: YOUR_BASE64_ENCODED_DB_PASSWORD
And a ConfigMap for non-sensitive configuration:
apiVersion: v1 kind: ConfigMap metadata: name: laravel-config data: APP_ENV: "production" APP_URL: "https://your-app.example.com" # Add other non-sensitive .env variables here
Your application pods will then mount these ConfigMaps and Secrets as environment variables, as demonstrated in the Deployment manifest earlier. This decouples configuration and secrets from your container image, enhancing security and manageability.
CI/CD Pipeline Integration with AWS ECR and EKS
A robust CI/CD pipeline is paramount. We’ll outline a workflow using AWS CodePipeline, CodeBuild, and ECR to build, test, and deploy our Dockerized Laravel application to EKS.
The pipeline typically involves these stages:
- Source Stage: Triggered by code commits to a repository (e.g., AWS CodeCommit, GitHub).
- Build Stage (AWS CodeBuild):
- Checks out code.
- Builds the Docker image using the
Dockerfile. - Pushes the image to AWS ECR (Elastic Container Registry).
- Runs unit and integration tests.
- Deploy Stage (AWS CodePipeline/EKS):
- Updates the Kubernetes
Deploymentresource to pull the new image from ECR. This can be done by updating the image tag in the Kubernetes manifest and applying it, or by using tools like Argo CD or Flux CD for GitOps-based deployments.
- Updates the Kubernetes
A simplified buildspec.yml for AWS CodeBuild:
version: 0.2
phases:
install:
runtime-versions:
php: 8.2 # Ensure the runtime matches your Dockerfile
commands:
- echo "Installing dependencies..."
- apt-get update -y && apt-get install -y docker.io && rm -rf /var/lib/apt/lists/*
- echo "Logging in to Amazon ECR..."
- aws ecr get-login-password --region $AWS_DEFAULT_REGION | docker login --username AWS --password-stdin $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com
- REPOSITORY_URI=$AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME
- COMMIT_HASH=$(echo $CODEBUILD_RESOLVED_SOURCE_VERSION | cut -c1-7)
- IMAGE_TAG=$COMMIT_HASH-$CODEBUILD_BUILD_ID
pre_build:
commands:
- echo "Pulling existing image if it exists..."
- docker pull $REPOSITORY_URI:$IMAGE_TAG || true
build:
commands:
- echo "Building the Docker image..."
- docker build -t $REPOSITORY_URI:$IMAGE_TAG .
- echo "Pushing the Docker image to ECR..."
- docker push $REPOSITORY_URI:$IMAGE_TAG
post_build:
commands:
- echo "Running tests..."
# Example: Run PHPUnit tests
- docker run --rm -v $(pwd):/app -w /app php:8.2-cli php artisan test
- echo "Deployment preparation complete."
# If using kubectl directly, configure kubectl here and apply manifests
# - echo "Deploying to EKS..."
# - aws eks update-kubeconfig --name $EKS_CLUSTER_NAME --region $AWS_DEFAULT_REGION
# - sed "s|YOUR_ECR_REPO/laravel-app:latest|$REPOSITORY_URI:$IMAGE_TAG|g" kubernetes/deployment.yaml > kubernetes/deployment-temp.yaml
# - kubectl apply -f kubernetes/deployment-temp.yaml
# - rm kubernetes/deployment-temp.yaml
# - kubectl apply -f kubernetes/service.yaml
# - kubectl apply -f kubernetes/ingress.yaml
# - kubectl apply -f kubernetes/hpa.yaml
# Define environment variables for CodeBuild
# These would be set in the CodePipeline/CodeBuild configuration
# e.g., IMAGE_REPO_NAME, AWS_ACCOUNT_ID, EKS_CLUSTER_NAME
The deployment step in CodePipeline can then be configured to trigger an EKS update. For more advanced GitOps workflows, tools like Argo CD or Flux CD can monitor a Git repository containing your Kubernetes manifests and automatically apply changes when new image tags are updated in ECR or referenced in Git.
Monitoring and Logging Strategies
Effective monitoring and logging are critical for maintaining a healthy Kubernetes cluster and application. We’ll leverage Prometheus for metrics and Fluentd/Fluent Bit for log aggregation.
Metrics:
- Prometheus: Deploy Prometheus to your EKS cluster. It can scrape metrics from various sources, including Kubernetes itself, node exporters, and application-specific metrics.
- Application Metrics: Instrument your Laravel application to expose metrics. Libraries like
prometheus_clientfor PHP can be used. Expose these metrics via an HTTP endpoint (e.g.,/metrics) that Prometheus can scrape. - Alerting: Configure Alertmanager to send notifications based on Prometheus alerts (e.g., high error rates, low replica counts, high latency).
Logging:
- Fluentd/Fluent Bit DaemonSet: Deploy Fluentd or Fluent Bit as a DaemonSet on your EKS cluster. This ensures a logging agent runs on each node.
- Log Aggregation: Configure the DaemonSet to collect logs from all containers (including PHP-FPM, queue workers, etc.) and forward them to a centralized logging system like AWS CloudWatch Logs, Elasticsearch, or Splunk.
- Structured Logging: Ensure your Laravel application logs in a structured format (e.g., JSON). This makes log parsing and querying much easier in your centralized logging system. You can achieve this by using a custom log formatter in Laravel’s logging configuration.
Example of a basic structured log formatter in Laravel (app/Providers/AppServiceProvider.php):
use Illuminate\Support\ServiceProvider;
use Monolog\Formatter\JsonFormatter;
use Monolog\Handler\StreamHandler;
use Monolog\Logger;
class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*/
public function register(): void
{
//
}
/**
* Bootstrap any application services.
*/
public function boot(): void
{
$this->app->extend('log', function ($log) {
$handler = new StreamHandler(storage_path('logs/laravel.log'), Logger::DEBUG);
$handler->setFormatter(new JsonFormatter());
$log->getMonolog()->pushHandler($handler);
return $log;
});
}
}
This ensures that logs generated by Laravel’s logging facade will be in JSON format, simplifying ingestion and analysis by log aggregation tools.