Leveraging Laravel Forge & Envoyer for Zero-Downtime Deployments with Dockerized PHP 9 Microservices on AWS EKS
Architectural Overview: Microservices, Docker, EKS, Forge, and Envoyer
This document outlines a robust, scalable, and zero-downtime deployment strategy for PHP 9 microservices orchestrated on AWS Elastic Kubernetes Service (EKS). We leverage Laravel Forge for infrastructure provisioning and initial setup, and Laravel Envoyer for sophisticated, zero-downtime application deployments. The core of our deployment pipeline relies on Docker containers managed by Kubernetes, ensuring consistency across environments and enabling seamless scaling.
Provisioning EKS Cluster with Laravel Forge
While Forge is primarily known for single-server deployments, its underlying infrastructure management capabilities can be extended to provision and configure the foundational AWS resources required for an EKS cluster. This involves setting up VPCs, subnets, security groups, and IAM roles. For direct EKS cluster creation, we’ll typically use AWS CLI or Terraform, but Forge can manage the EC2 instances that will act as worker nodes.
First, ensure your AWS credentials are configured correctly for Forge. Then, within your Forge dashboard, navigate to the “Servers” section and click “Create Server.” Select “Custom Server” and choose your desired region and instance type. Forge will then guide you through the process of connecting to your AWS account and launching an EC2 instance. This instance will serve as a base for our Kubernetes worker nodes or a management node if we’re not using EKS managed node groups.
For a production-grade EKS setup, we’ll use AWS’s managed node groups. Forge can provision the EC2 instances, but the EKS control plane and managed node groups are best provisioned via AWS CLI or Infrastructure as Code (IaC) tools like Terraform. Here’s a conceptual Terraform snippet for provisioning an EKS cluster and a managed node group:
# main.tf
provider "aws" {
region = "us-east-1"
}
resource "aws_eks_cluster" "microservices_cluster" {
name = "microservices-eks-cluster"
role_arn = aws_iam_role.eks_cluster_role.arn
vpc_config {
subnet_ids = ["subnet-xxxxxxxxxxxxxxxxx", "subnet-yyyyyyyyyyyyyyyyy"] # Replace with your actual subnet IDs
}
# ... other EKS cluster configurations
}
resource "aws_eks_node_group" "microservices_nodes" {
cluster_name = aws_eks_cluster.microservices_cluster.name
node_group_name = "microservices-worker-nodes"
node_role_arn = aws_iam_role.eks_node_role.arn
subnet_ids = ["subnet-xxxxxxxxxxxxxxxxx", "subnet-yyyyyyyyyyyyyyyyy"] # Replace with your actual subnet IDs
scaling_config {
desired_size = 3
max_size = 5
min_size = 2
}
# ... other node group configurations
}
resource "aws_iam_role" "eks_cluster_role" {
name = "eks-cluster-role"
# ... IAM policy attachments for EKS cluster
}
resource "aws_iam_role" "eks_node_role" {
name = "eks-node-role"
# ... IAM policy attachments for EKS worker nodes
}
Once the EKS cluster is provisioned, you’ll need to configure kubectl to communicate with it. This typically involves fetching the cluster’s configuration using the AWS CLI:
aws eks update-kubeconfig --region us-east-1 --name microservices-eks-cluster
Dockerizing PHP 9 Microservices
Each microservice will be containerized using Docker. The PHP 9 image should be lean, secure, and optimized for production. We’ll use a multi-stage build to keep the final image size minimal.
Consider a typical microservice, say, a user authentication service. Its Dockerfile might look like this:
# Dockerfile for User Authentication Microservice
# Stage 1: Builder
FROM php:8.3-fpm AS builder
WORKDIR /app
# Install necessary extensions and dependencies
RUN apt-get update && apt-get install -y \
git \
unzip \
libzip-dev \
libpng-dev \
libjpeg-dev \
libfreetype6-dev \
libonig-dev \
libxml2-dev \
libssl-dev \
zlib1g-dev \
libicu-dev \
&& docker-php-ext-configure gd --with-freetype --with-jpeg \
&& docker-php-ext-install -j$(nproc) gd mbstring zip pdo_mysql opcache intl bcmath \
&& pecl install redis \
&& docker-php-ext-enable redis \
&& curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer \
&& rm -rf /var/lib/apt/lists/*
COPY . /app
# Install Composer dependencies
RUN composer install --no-dev --optimize-autoloader --no-interaction
# Stage 2: Production Image
FROM php:8.3-fpm-alpine
WORKDIR /app
# Copy only necessary files from builder stage
COPY --from=builder /usr/local/bin/composer /usr/local/bin/composer
COPY --from=builder /app/vendor /app/vendor
COPY --from=builder /app/public /app/public
COPY --from=builder /app/bootstrap /app/bootstrap
COPY --from=builder /app/config /app/config
COPY --from=builder /app/routes /app/routes
COPY --from=builder /app/app /app/app
COPY --from=builder /app/.env.example /app/.env.example # Copy example env file
# Install runtime dependencies
RUN apk add --no-cache \
libzip \
libpng \
libjpeg-turbo \
freetype2 \
icu-data-full \
oniguruma-dev \
libxml2-dev \
libssl-dev \
zlib-dev
# Enable PHP extensions needed at runtime
RUN docker-php-ext-enable \
gd \
mbstring \
zip \
pdo_mysql \
opcache \
intl \
bcmath \
redis
# Copy nginx configuration if using a combined image or for local testing
# COPY nginx.conf /etc/nginx/sites-available/default
# Expose port and set entrypoint/cmd
EXPOSE 9000
CMD ["php-fpm"]
For production, we’ll typically use a separate Nginx container as a reverse proxy. The PHP-FPM container will expose port 9000. The Docker image should be built and pushed to a container registry accessible by EKS, such as Amazon ECR.
# Build the Docker image docker build -t YOUR_AWS_ACCOUNT_ID.dkr.ecr.YOUR_REGION.amazonaws.com/user-service:latest . # Authenticate Docker to your ECR registry aws ecr get-login-password --region YOUR_REGION | docker login --username AWS --password-stdin YOUR_AWS_ACCOUNT_ID.dkr.ecr.YOUR_REGION.amazonaws.com # Push the image to ECR docker push YOUR_AWS_ACCOUNT_ID.dkr.ecr.YOUR_REGION.amazonaws.com/user-service:latest
Kubernetes Deployment and Service Definitions
We’ll define Kubernetes resources using YAML manifests. This includes Deployments for managing our microservice pods and Services for exposing them internally and externally.
Here’s a sample deployment.yaml for the user service:
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: user-service-deployment
labels:
app: user-service
spec:
replicas: 3 # Start with 3 replicas for high availability
selector:
matchLabels:
app: user-service
template:
metadata:
labels:
app: user-service
spec:
containers:
- name: user-service
image: YOUR_AWS_ACCOUNT_ID.dkr.ecr.YOUR_REGION.amazonaws.com/user-service:latest # Replace with your ECR image
ports:
- containerPort: 9000
env:
- name: DB_HOST
value: "mysql-service" # Assuming a separate MySQL microservice or RDS
- name: DB_PORT
value: "3306"
- name: DB_DATABASE
value: "users_db"
- name: REDIS_HOST
value: "redis-service" # Assuming a separate Redis microservice or ElastiCache
# Add other environment variables as needed
resources:
requests:
memory: "64Mi"
cpu: "250m"
limits:
memory: "128Mi"
cpu: "500m"
# Add readiness and liveness probes for robust health checking
readinessProbe:
httpGet:
path: /health # Assuming a /health endpoint in your microservice
port: 9000
initialDelaySeconds: 15
periodSeconds: 10
livenessProbe:
httpGet:
path: /health
port: 9000
initialDelaySeconds: 30
periodSeconds: 20
And a corresponding service.yaml to expose the deployment internally:
# service.yaml
apiVersion: v1
kind: Service
metadata:
name: user-service
spec:
selector:
app: user-service
ports:
- protocol: TCP
port: 80 # Internal port for the service
targetPort: 9000 # Port the container is listening on
type: ClusterIP # Internal service type
To deploy these to your EKS cluster:
kubectl apply -f deployment.yaml kubectl apply -f service.yaml
Ingress Controller for External Access
For external access to your microservices, an Ingress controller is essential. AWS Load Balancer Controller is a popular choice for EKS, automatically provisioning an AWS Application Load Balancer (ALB) for your services.
First, install the AWS Load Balancer Controller in your EKS cluster. This typically involves applying a set of Kubernetes manifests and configuring IAM permissions.
# Example: Install AWS Load Balancer Controller (refer to AWS documentation for the latest version and instructions) kubectl apply -k "github.com/aws/eks-charts/stable/aws-load-balancer-controller//crds?ref=master" kubectl apply -f https://raw.githubusercontent.com/aws/eks-charts/master/stable/aws-load-balancer-controller/chart/values.yaml kubectl apply -f https://raw.githubusercontent.com/aws/eks-charts/master/stable/aws-load-balancer-controller/crds/base/ingress. பாதி.io/ingressclasses.yaml kubectl apply -f https://raw.githubusercontent.com/aws/eks-charts/master/stable/aws-load-balancer-controller/crds/base/ingress. பாதி.io/ingresses.yaml # ... and the controller deployment itself
Then, create an ingress.yaml to route traffic to your microservices:
# ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: microservices-ingress
annotations:
kubernetes.io/ingress.class: alb # Specify the ALB Ingress controller
alb.ingress.kubernetes.io/scheme: internet-facing
alb.ingress.kubernetes.io/target-type: ip # Or instance, depending on your setup
alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 80}, {"HTTPS":443}]'
# Add SSL certificate ARN if using HTTPS
# alb.ingress.kubernetes.io/certificate-arn: "arn:aws:acm:YOUR_REGION:YOUR_ACCOUNT_ID:certificate/YOUR_CERT_ID"
spec:
rules:
- host: api.yourdomain.com # Your API domain
http:
paths:
- path: /users
pathType: Prefix
backend:
service:
name: user-service
port:
number: 80
- path: /products
pathType: Prefix
backend:
service:
name: product-service # Assuming a product-service
port:
number: 80
# Add TLS configuration if using HTTPS
# tls:
# - hosts:
# - api.yourdomain.com
# secretName: your-tls-secret # Kubernetes secret containing your TLS certificate
Apply the Ingress resource:
kubectl apply -f ingress.yaml
This will provision an ALB, and you can find its DNS name using kubectl get ingress microservices-ingress. Update your domain’s DNS records to point to this ALB DNS name.
Zero-Downtime Deployments with Laravel Envoyer
Laravel Envoyer is designed for zero-downtime deployments. While Envoyer traditionally deploys to individual servers, we can adapt its workflow to orchestrate deployments to our EKS cluster. This involves using Envoyer to trigger deployment scripts that interact with Kubernetes.
Envoyer Setup:
- Create a new project in Envoyer.
- Select “Custom Deployment” as the server type.
- Add a server. The IP address and SSH credentials will be used to connect to a bastion host or a management node within your AWS VPC that has
kubectlconfigured to access your EKS cluster. - Configure the deployment script.
Custom Deployment Script (Conceptual):
The core of this strategy is a custom deployment script executed by Envoyer. This script will perform the following actions:
- Fetch the latest Docker image from ECR.
- Update the Kubernetes Deployment with the new image tag.
- Utilize Kubernetes rolling update strategy to replace old pods with new ones gradually.
#!/bin/bash
# --- Configuration ---
MICROSERVICE_NAME="user-service"
ECR_IMAGE="YOUR_AWS_ACCOUNT_ID.dkr.ecr.YOUR_REGION.amazonaws.com/${MICROSERVICE_NAME}"
KUBECTL_CONTEXT="microservices-eks-cluster" # Your EKS kubectl context name
DEPLOYMENT_FILE="deployment.yaml" # Path to your deployment manifest
# --- Script Logic ---
echo "Starting zero-downtime deployment for ${MICROSERVICE_NAME}..."
# 1. Authenticate to AWS ECR (if not already done by the bastion/management host)
# aws ecr get-login-password --region YOUR_REGION | docker login --username AWS --password-stdin YOUR_AWS_ACCOUNT_ID.dkr.ecr.YOUR_REGION.amazonaws.com
# 2. Determine the latest image tag (e.g., from Git commit hash or a CI/CD pipeline variable)
# For simplicity, we'll use a placeholder. In a real CI/CD, this would be dynamic.
NEW_IMAGE_TAG=$(git rev-parse --short HEAD) # Example: Use Git commit hash
FULL_IMAGE_NAME="${ECR_IMAGE}:${NEW_IMAGE_TAG}"
echo "Using image: ${FULL_IMAGE_NAME}"
# 3. Update the Kubernetes Deployment manifest
# This is a crucial step. We need to update the image tag in the deployment.yaml.
# A common approach is to use `sed` or `yq` (a YAML processor).
# Ensure your deployment.yaml has a placeholder for the image tag or use a templating tool.
# Example using sed (be cautious with complex YAML structures)
sed -i "s|image: ${ECR_IMAGE}:.*|image: ${FULL_IMAGE_NAME}|" ${DEPLOYMENT_FILE}
echo "Updated ${DEPLOYMENT_FILE} with new image tag."
# 4. Apply the updated deployment to EKS
# Ensure kubectl is configured to use the correct context.
export KUBECONFIG=/path/to/your/.kube/config # Or ensure it's set in the environment
echo "Applying updated deployment to Kubernetes..."
kubectl --context ${KUBECTL_CONTEXT} apply -f ${DEPLOYMENT_FILE}
# 5. Monitor the rollout status
echo "Monitoring deployment rollout..."
kubectl --context ${KUBECTL_CONTEXT} rollout status deployment/${MICROSERVICE_NAME}-deployment --timeout=5m
if [ $? -eq 0 ]; then
echo "Deployment of ${MICROSERVICE_NAME} successful!"
else
echo "Deployment of ${MICROSERVICE_NAME} failed. Check rollout status and logs."
# Optionally, trigger a rollback or alert
exit 1
fi
echo "Deployment complete."
Envoyer Deployment Steps:
- Configure Envoyer to connect to your bastion host/management node.
- Upload your application code (including the updated
deployment.yaml) to the Envoyer server. - Execute the custom deployment script.
Important Considerations for Envoyer Integration:
- Security: The bastion host must have secure SSH access and the necessary IAM permissions to interact with EKS.
- Kubernetes Configuration: Ensure the
.kube/configfile on the bastion host is correctly configured for your EKS cluster. - Image Tagging: Use a robust image tagging strategy (e.g., Git commit SHAs, semantic versioning) to ensure you can roll back if necessary.
- Rollback Strategy: Implement automated rollbacks in your script or Envoyer’s failure handling if the rollout status indicates an issue.
- CI/CD Integration: For a fully automated pipeline, integrate this process into a CI/CD tool like GitHub Actions, GitLab CI, or Jenkins. The CI/CD tool would build and push the Docker image, then trigger Envoyer or directly apply Kubernetes manifests.
Database Migrations and State Management
Database migrations require careful handling in a microservices architecture, especially with zero-downtime deployments. The strategy is to ensure backward compatibility between the old and new versions of your microservice during the rolling update.
Recommended Approach:
- Migrations as a Separate Job: Run database migrations as a Kubernetes Job before initiating the rolling update of the microservice Deployment. This ensures the database schema is updated before new application pods come online.
- Backward Compatibility: The new version of your microservice must be able to read data written by the old version, and the old version must be able to read data written by the new version. This often means:
- New fields are nullable or have default values.
- Avoid dropping columns or making breaking schema changes in the same deployment as the application code that relies on those changes.
- Rollback: If a rollback is necessary, you might need to revert database schema changes as well, which can be complex. Consider using tools like Flyway or Liquibase for robust migration management.
# migration-job.yaml
apiVersion: batch/v1
kind: Job
metadata:
name: user-service-migrations
labels:
app: user-service
spec:
template:
spec:
containers:
- name: migration-runner
image: YOUR_AWS_ACCOUNT_ID.dkr.ecr.YOUR_REGION.amazonaws.com/user-service:latest # Use the same image as the deployment
command: ["php", "artisan", "migrate", "--force"] # Use --force in production
env:
- name: DB_HOST
value: "mysql-service"
- name: DB_PORT
value: "3306"
- name: DB_DATABASE
value: "users_db"
# ... other database credentials
restartPolicy: Never # Jobs should not restart indefinitely
backoffLimit: 4 # Number of retries before marking the job as failed
You would apply this job before applying the updated deployment manifest in your Envoyer script.
Monitoring and Alerting
A comprehensive monitoring and alerting strategy is critical for maintaining a healthy EKS environment. Integrate tools like Prometheus and Grafana for metrics collection and visualization, and Alertmanager for notifications.
Key Metrics to Monitor:
- Pod health (CPU, Memory usage, restarts)
- Deployment rollout status
- Ingress controller health and latency
- Application-specific metrics (e.g., request rates, error rates, queue lengths)
- Database performance
Configure alerts for critical conditions such as high error rates, pod restarts, or failed deployments. Integrate these alerts with your incident management system (e.g., PagerDuty, Opsgenie).
Conclusion
By combining Laravel Forge for infrastructure scaffolding, Docker for containerization, AWS EKS for orchestration, and Laravel Envoyer for sophisticated deployment workflows, you can achieve highly available, scalable, and zero-downtime deployments for your PHP 9 microservices. The key is meticulous planning of Kubernetes manifests, robust container images, and a well-defined, automated deployment script that leverages Kubernetes’ rolling update capabilities.