Beyond the Basics: Mastering Kubernetes for High-Availability Laravel Deployments with GitOps and CI/CD Automation
Kubernetes Cluster Setup for High Availability
Achieving high availability for a Laravel application on Kubernetes necessitates a robust cluster configuration. This involves not just deploying your application but also ensuring critical components like the database, caching layer, and the application itself are resilient to failures. We’ll focus on a multi-node setup with proper resource allocation and fault tolerance.
For this guide, we assume you have a Kubernetes cluster provisioned. This could be a managed service (GKE, EKS, AKS) or a self-hosted solution using tools like `kubeadm`. The key is having multiple worker nodes distributed across availability zones if possible.
Database High Availability: PostgreSQL with Patroni
A single database instance is a single point of failure. For a highly available Laravel application, we need a replicated database solution. PostgreSQL with Patroni is an excellent choice, providing automated failover and replication management.
First, we need a Kubernetes Secret to store the database credentials. Ensure this secret is created in the same namespace where you’ll deploy Patroni.
apiVersion: v1 kind: Secret metadata: name: postgresql-secret namespace: default type: Opaque data: # Base64 encoded values for: # POSTGRES_USER: myuser # POSTGRES_PASSWORD: mypassword # POSTGRES_DB: mydatabase POSTGRES_USER: bXl1c2Vy POSTGRES_PASSWORD: bXlwYXNzd29yZA== POSTGRES_DB: bXlkYXRhYmFzZQ==
Next, we’ll deploy PostgreSQL using the official Docker image and configure Patroni. Patroni requires a distributed configuration store; etcd is a common and reliable choice. We’ll assume you have an etcd cluster accessible or deploy a small one within Kubernetes for simplicity (though a dedicated etcd cluster is recommended for production).
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: postgresql
namespace: default
spec:
serviceName: postgresql
replicas: 3 # Number of PostgreSQL replicas
selector:
matchLabels:
app: postgresql
template:
metadata:
labels:
app: postgresql
spec:
containers:
- name: postgresql
image: postgres:14-alpine
ports:
- containerPort: 5432
env:
- name: POSTGRES_USER
valueFrom:
secretKeyRef:
name: postgresql-secret
key: POSTGRES_USER
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: postgresql-secret
key: POSTGRES_PASSWORD
- name: POSTGRES_DB
valueFrom:
secretKeyRef:
name: postgresql-secret
key: POSTGRES_DB
volumeMounts:
- name: postgresql-persistent-storage
mountPath: /var/lib/postgresql/data
volumeClaimTemplates:
- metadata:
name: postgresql-persistent-storage
spec:
accessModes: [ "ReadWriteOnce" ]
resources:
requests:
storage: 10Gi # Adjust storage as needed
---
apiVersion: v1
kind: Service
metadata:
name: postgresql-read
namespace: default
spec:
selector:
app: postgresql
ports:
- protocol: TCP
port: 5432
targetPort: 5432
clusterIP: None # Headless service for StatefulSet
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: patroni
namespace: default
spec:
serviceName: patroni
replicas: 3 # Should match PostgreSQL replicas
selector:
matchLabels:
app: patroni
template:
metadata:
labels:
app: patroni
spec:
containers:
- name: patroni
image: zalando/patroni:latest # Use a specific version in production
command: ["patroni"]
args:
- "/etc/patroni/patroni.yml"
env:
- name: ETCD_HOSTS
value: "etcd.default.svc.cluster.local:2379" # Adjust if etcd is in a different namespace
- name: POSTGRES_USER
valueFrom:
secretKeyRef:
name: postgresql-secret
key: POSTGRES_USER
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: postgresql-secret
key: POSTGRES_PASSWORD
- name: POSTGRES_DB
valueFrom:
secretKeyRef:
name: postgresql-secret
key: POSTGRES_DB
ports:
- containerPort: 8008 # Patroni API port
- containerPort: 5432 # PostgreSQL port
volumeMounts:
- name: patroni-config-volume
mountPath: /etc/patroni
- name: postgresql-persistent-storage # Mount the same persistent storage for PostgreSQL data
mountPath: /var/lib/postgresql/data
volumeClaimTemplates:
- metadata:
name: postgresql-persistent-storage
spec:
accessModes: [ "ReadWriteOnce" ]
resources:
requests:
storage: 10Gi
---
apiVersion: v1
kind: ConfigMap
metadata:
name: patroni-config
namespace: default
data:
patroni.yml: |
scope: postgresql
namespace: default
restapi:
listen: 0.0.0.0:8008
etcd:
host: etcd.default.svc.cluster.local:2379 # Adjust if etcd is in a different namespace
protocol: http
postgresql:
listen: 0.0.0.0:5432
data_dir: /var/lib/postgresql/data
pg_hba:
- host all all 0.0.0.0/0 md5
parameters:
max_connections: 100
shared_buffers: 128MB
effective_cache_size: 256MB
tags:
nofailover: false
clonefrom: false
bootstrap:
dcs:
ttl: 30
loop_wait: 10
retry_timeout: 10
maximum_lag_on_failover: 1048576 # 1MB
postgresql:
use_pg_rewind: true
authentication:
superuser:
password: &PG_SUPERUSER_PASSWORD &PG_SUPERUSER_PASSWORD # Placeholder, will be overridden by env var
replication:
password: &PG_REPLICATION_PASSWORD &PG_REPLICATION_PASSWORD # Placeholder
parameters:
wal_level: replica
hot_standby: "on"
max_wal_senders: 5
max_replication_slots: 5
---
apiVersion: v1
kind: Service
metadata:
name: postgresql-master
namespace: default
spec:
selector:
app: patroni
ports:
- protocol: TCP
port: 5432
targetPort: 5432
type: ClusterIP
---
apiVersion: v1
kind: Service
metadata:
name: postgresql-replica
namespace: default
spec:
selector:
app: patroni
ports:
- protocol: TCP
port: 5432
targetPort: 5432
type: ClusterIP
# Note: This configuration uses a headless service for PostgreSQL StatefulSet
# and a ClusterIP service for Patroni to provide a stable endpoint for the master.
# For read replicas, you would typically use a separate service pointing to replicas.
# This example simplifies by using the Patroni master service for all connections.
# In a real-world scenario, you might configure Patroni to expose read-only endpoints
# or use a dedicated read replica service.
Explanation:
- PostgreSQL StatefulSet: Manages the PostgreSQL instances. Each pod gets a stable network identity and persistent storage. The `replicas` count determines the desired number of PostgreSQL nodes.
- Patroni StatefulSet: Manages the Patroni agents. Each Patroni agent communicates with etcd to manage the PostgreSQL cluster state.
- Patroni ConfigMap: Contains the Patroni configuration, pointing to etcd for state management and defining PostgreSQL parameters.
- Services:
postgresql-masterpoints to the current PostgreSQL primary, managed by Patroni.postgresql-read(headless) allows direct access to all PostgreSQL pods for debugging or specific use cases. - Secrets: Securely stores database credentials.
- Persistent Storage: Essential for PostgreSQL data. Ensure your Kubernetes cluster has a StorageClass configured for dynamic provisioning.
Redis for Caching and Session Management
Redis is crucial for Laravel’s performance. For high availability, we’ll deploy Redis Sentinel, which provides automatic failover for Redis instances.
apiVersion: v1
kind: ConfigMap
metadata:
name: redis-config
namespace: default
data:
redis.conf: |
appendonly yes
save 60 1
save 300 10
save 900 1
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: redis
namespace: default
spec:
serviceName: redis
replicas: 3 # Number of Redis master/replica instances
selector:
matchLabels:
app: redis
template:
metadata:
labels:
app: redis
spec:
containers:
- name: redis
image: redis:7-alpine
command: ["redis-server", "/usr/local/etc/redis/redis.conf"]
ports:
- containerPort: 6379
volumeMounts:
- name: redis-persistent-storage
mountPath: /data
- name: redis-config-volume
mountPath: /usr/local/etc/redis/redis.conf
subPath: redis.conf
volumeClaimTemplates:
- metadata:
name: redis-persistent-storage
spec:
accessModes: [ "ReadWriteOnce" ]
resources:
requests:
storage: 5Gi # Adjust storage as needed
---
apiVersion: v1
kind: Service
metadata:
name: redis-headless
namespace: default
spec:
selector:
app: redis
ports:
- protocol: TCP
port: 6379
targetPort: 6379
clusterIP: None # Headless service for StatefulSet
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: redis-sentinel
namespace: default
spec:
serviceName: redis-sentinel
replicas: 3 # Number of Sentinel instances
selector:
matchLabels:
app: redis-sentinel
template:
metadata:
labels:
app: redis-sentinel
spec:
containers:
- name: redis-sentinel
image: redis:7-alpine # Sentinel is included in the Redis image
command: ["redis-sentinel", "/etc/redis/sentinel.conf"]
ports:
- containerPort: 26379
volumeMounts:
- name: sentinel-config-volume
mountPath: /etc/redis/sentinel.conf
subPath: sentinel.conf
---
apiVersion: v1
kind: ConfigMap
metadata:
name: sentinel-config
namespace: default
data:
sentinel.conf: |
port 26379
sentinel monitor mymaster redis-headless 6379 2
sentinel down-after-milliseconds mymaster 5000
sentinel failover-timeout mymaster 10000
sentinel parallel-syncs mymaster 1
Explanation:
- Redis StatefulSet: Manages the Redis master and replica instances. We’ve set 3 replicas for a basic HA setup (one master, two replicas).
- Redis Headless Service: Allows Sentinel to discover and manage individual Redis pods.
- Redis Sentinel StatefulSet: Manages the Sentinel instances responsible for monitoring Redis and performing failovers.
- Sentinel ConfigMap: Configures Sentinel to monitor a master named
mymaster, which will be the primary Redis instance discovered via theredis-headlessservice. The quorum of2means at least two Sentinels must agree a master is down for a failover to occur.
Your Laravel application will connect to Redis using the Sentinel service. You’ll need to configure your config/database.php and config/cache.php (or relevant environment variables) to use the Sentinel cluster.
Laravel Application Deployment with GitOps
GitOps is a paradigm where Git is the single source of truth for declarative infrastructure and applications. Changes to Git trigger automated deployments. We’ll use Argo CD as our GitOps tool.
Prerequisites:
- A Kubernetes cluster.
- Argo CD installed in your cluster.
- A Git repository containing your Laravel application code and Kubernetes manifests.
1. Prepare Your Laravel Application Repository:
Ensure your Laravel application is containerized using a Dockerfile. Your repository should contain:
Dockerfilefor your Laravel application.- Kubernetes manifests (Deployments, Services, Ingress, etc.) for your application.
- A
.gitopsdirectory (or similar) to store your Argo CD Application definition.
Example Dockerfile:
FROM php:8.2-fpm-alpine
# Install system dependencies
RUN apk update && apk add --no-cache \
git \
zip \
unzip \
icu-dev \
libzip-dev \
libpng-dev \
jpeg-dev \
freetype-dev \
libjpeg-turbo-dev \
libwebp-dev \
libpng-dev \
libxml2-dev \
postgresql-dev \
supervisor \
nginx
# Install PHP extensions
RUN docker-php-ext-configure gd --with-freetype --with-jpeg --with-webp \
&& docker-php-ext-install -j$(nproc) gd \
&& docker-php-ext-install pdo pdo_pgsql zip intl opcache
# Install Composer
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
# Set working directory
WORKDIR /var/www/html
# Copy application code
COPY . .
# Install dependencies
RUN composer install --no-dev --optimize-autoloader
# Permissions
RUN chown -R www-data:www-data storage bootstrap/cache && chmod -R 775 storage bootstrap/cache
# Copy Nginx configuration
COPY docker/nginx.conf /etc/nginx/conf.d/default.conf
# Copy Supervisor configuration
COPY docker/supervisord.conf /etc/supervisor/conf.d/supervisord.conf
# Expose ports
EXPOSE 80
EXPOSE 9000
# Start Supervisor
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/supervisord.conf"]
Example Nginx Configuration (docker/nginx.conf):
server {
listen 80;
server_name localhost;
root /var/www/html/public;
index index.php index.html index.htm;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_pass unix:/var/run/php/php8.2-fpm.sock;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
}
location ~ /\.ht {
deny all;
}
}
Example Supervisor Configuration (docker/supervisord.conf):
[supervisord] nodaemon=true user=root [program:php-fpm] command=/usr/sbin/php-fpm8.2 -y /etc/php/8.2/fpm/php-fpm.conf autostart=true autorestart=true user=www-data stdout_logfile=/dev/stdout stdout_logfile_maxbytes=0 stderr_logfile=/dev/stderr stderr_logfile_maxbytes=0 [program:nginx] command=/usr/sbin/nginx -g "daemon off;" autostart=true autorestart=true user=root stdout_logfile=/dev/stdout stdout_logfile_maxbytes=0 stderr_logfile=/dev/stderr stderr_logfile_maxbytes=0
2. Kubernetes Manifests for Laravel Application:
apiVersion: apps/v1
kind: Deployment
metadata:
name: laravel-app
namespace: default
spec:
replicas: 3 # Adjust based on expected load
selector:
matchLabels:
app: laravel-app
template:
metadata:
labels:
app: laravel-app
spec:
containers:
- name: laravel-app
image: your-docker-registry/your-laravel-app:latest # Replace with your image
ports:
- containerPort: 80
env:
- name: APP_ENV
value: "production"
- name: APP_KEY
valueFrom:
secretKeyRef:
name: laravel-app-secrets
key: APP_KEY
- name: APP_URL
value: "https://your-domain.com" # Or your ingress host
- name: DB_HOST
value: "postgresql-master.default.svc.cluster.local" # Patroni master service
- name: DB_PORT
value: "5432"
- name: DB_USERNAME
valueFrom:
secretKeyRef:
name: postgresql-secret
key: POSTGRES_USER
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: postgresql-secret
key: POSTGRES_PASSWORD
- name: DB_DATABASE
valueFrom:
secretKeyRef:
name: postgresql-secret
key: POSTGRES_DB
- name: CACHE_DRIVER
value: "redis"
- name: REDIS_HOST
value: "redis-sentinel.default.svc.cluster.local" # Redis Sentinel service
- name: REDIS_PORT
value: "26379" # Sentinel port
- name: REDIS_PASSWORD
value: null # If your Redis doesn't require a password
# Add other Laravel environment variables as needed
readinessProbe:
httpGet:
path: /healthz # Create a health check endpoint in Laravel
port: 80
initialDelaySeconds: 15
periodSeconds: 10
livenessProbe:
httpGet:
path: /healthz
port: 80
initialDelaySeconds: 30
periodSeconds: 20
---
apiVersion: v1
kind: Service
metadata:
name: laravel-app-service
namespace: default
spec:
selector:
app: laravel-app
ports:
- protocol: TCP
port: 80
targetPort: 80
type: ClusterIP
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: laravel-app-ingress
namespace: default
annotations:
# Add your ingress controller specific annotations here (e.g., for Nginx Ingress)
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
rules:
- host: your-domain.com # Replace with your domain
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: laravel-app-service
port:
number: 80
---
apiVersion: v1
kind: Secret
metadata:
name: laravel-app-secrets
namespace: default
type: Opaque
data:
APP_KEY: [base64-encoded-app-key] # Generate with `php artisan key:generate --show` and base64 encode
3. Argo CD Application Definition:
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: laravel-app
namespace: argocd # Namespace where Argo CD is installed
spec:
project: default
source:
repoURL: https://github.com/your-username/your-laravel-repo.git # Your Git repo URL
targetRevision: HEAD # Or a specific branch/tag
path: k8s/production # Path to your Kubernetes manifests in the repo
destination:
server: https://kubernetes.default.svc # Target Kubernetes cluster
namespace: default # Target namespace for deployment
syncPolicy:
automated:
prune: true # Automatically delete resources that are no longer defined in Git
selfHeal: true # Automatically sync if the cluster state drifts from Git
syncOptions:
- CreateNamespace=true
Apply this Argo CD Application manifest to your cluster:
kubectl apply -f argo-app.yaml -n argocd
Argo CD will now monitor your Git repository. When you push changes to the specified path (e.g., k8s/production), Argo CD will automatically apply those changes to your Kubernetes cluster, ensuring your Laravel application is deployed and updated according to the state defined in Git.
CI/CD Automation with GitHub Actions
Automating the build and deployment pipeline is crucial for rapid iteration and reliability. We’ll use GitHub Actions to build Docker images, push them to a registry, and update the GitOps repository.
1. GitHub Actions Workflow (.github/workflows/deploy.yml):
name: Laravel CI/CD
on:
push:
branches:
- main # Or your primary deployment branch
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push Docker image
id: docker_build
uses: docker/build-push-action@v4
with:
context: .
file: ./Dockerfile
push: true
tags: your-docker-registry/your-laravel-app:${{ github.sha }} # Tag with commit SHA
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Checkout GitOps repository
uses: actions/checkout@v3
with:
repository: your-username/your-gitops-repo # Your GitOps repo
path: gitops-repo
token: ${{ secrets.GIT_PAT }} # Personal Access Token for the GitOps repo
- name: Update Kubernetes manifest in GitOps repo
run: |
cd gitops-repo
# Update the image tag in your Kubernetes deployment manifest
sed -i 's|image: your-docker-registry/your-laravel-app:.*|image: your-docker-registry/your-laravel-app:${{ github.sha }}|g' k8s/production/deployment.yaml
# Commit and push the changes
git config --global user.name 'GitHub Actions'
git config --global user.email '[email protected]'
git add .
git commit -m "Update Laravel app image to ${{ github.sha }}"
git push origin main # Or your GitOps repo's main branch
env:
GIT_PAT: ${{ secrets.GIT_PAT }}
Explanation:
- Checkout code: Clones your Laravel application repository.
- Set up Docker Buildx: Enables multi-platform builds and better caching.
- Login to Docker Hub: Authenticates with your container registry.
- Build and push Docker image: Builds the Docker image for your Laravel app and pushes it to the registry, tagged with the Git commit SHA for traceability.
- Checkout GitOps repository: Clones your separate GitOps repository where your Kubernetes manifests reside.
- Update Kubernetes manifest: Uses
sedto find and replace the image tag in your deployment manifest with the newly built image’s tag. - Commit and push: Commits the updated manifest to the GitOps repository and pushes it. This push will trigger Argo CD to synchronize the changes to your Kubernetes cluster.
Important:
- Replace placeholders like
your-docker-registry,your-laravel-app,your-username/your-gitops-repo,your-domain.com, and the GitOps path (k8s/production/deployment.yaml) with your actual values. - Ensure you have set up the necessary secrets in your GitHub repository:
DOCKERHUB_USERNAME,DOCKERHUB_TOKEN, andGIT_PAT(a Personal Access Token with repository write access for your GitOps repo). - The
APP_KEYsecret needs to be generated and base64 encoded. - The
healthzendpoint in your Laravel app should return a 200 OK status.
Monitoring and Alerting
A high-availability setup is incomplete without robust monitoring and alerting. Prometheus and Grafana are standard tools for this in Kubernetes.
1. Prometheus Setup:
Deploy Prometheus using the Prometheus Operator or a Helm chart. Ensure it scrapes metrics from your application pods (via annotations on your Service or Deployment) and the database/Redis components.
2. Grafana Setup:
Deploy Grafana and configure it to use Prometheus as a data source. You can import pre-built dashboards for Kubernetes, PostgreSQL, and Redis, or create custom ones.
3. Alertmanager:
Configure Alertmanager (often deployed alongside Prometheus) to define alerting rules. For example, you might want alerts for:
- PostgreSQL primary unavailability.
- Redis Sentinel failover events.
- High error rates in your Laravel application (e.g., 5xx errors).
- Pod restarts.
- Resource saturation (CPU/Memory).
Integrate Alertmanager with your notification channels (Slack, PagerDuty, email) to ensure timely responses to incidents.
Conclusion
This comprehensive setup leverages Kubernetes for orchestration, Patroni for database HA, Redis Sentinel for caching HA, GitOps with Argo CD for declarative deployments, and GitHub Actions for CI/CD automation. By combining these technologies, you can achieve a highly available, resilient, and easily manageable Laravel deployment.