Wordpress is a common Content Management System (CMS) for building websites and blogs. Scaling Wordpress can be difficult, especially in the cloud due to the shared file system requirement for uploads, plugins, and themes. AWS publishes a document with best practices for running highly scalable Wordpress installations on AWS. This document follows Wordpress’ recommendation for putting the entire Wordpress codebase in an Elastic File System (EFS) mount. This means every request is relying an NFS backed application. At scale, this can become sluggish and cause issues.
It can be difficult to translate this document to a container deployment on AWS Elastic Kubernetes Service (EKS). The principles of this document can be applied to any Kubernetes deployment on AWS. In this post, we will walk through provisioning the Kubernetes cluster, setting up the Elastic File System (EFS) Container Storage Interface (CSI) driver, and configuring the ALB Ingress Controller.
In this post, I propose version controlling all plugins and themes within your site. You can install and update these during a Docker build using the Wordpress CLI. An example of this Dockerfile can be found in the companion Github repository.
To get started, we need to deploy our cluster. This will take about 20 minutes to deploy. We will use the eksctl to deploy our cluster.
# ./cluster.yml
apiVersion: eksctl.io/v1alpha5
kind: ClusterConfig
metadata:
name: cms
region: us-east-2
vpc:
cidr: "10.1.0.0/16"
clusterEndpoints:
publicAccess: true
privateAccess: true
cloudWatch:
clusterLogging:
enableTypes: ["audit", "authenticator"]
nodeGroups:
- name: NodeGroup1
instanceType: t3.large
desiredCapacity: 3
privateNetworking: true
ssh:
publicKeyPath: ~/.ssh/id_rsa.pub
tags:
k8s.io/cluster-autoscaler/enabled: "true"
k8s.io/cluster-autoscaler/cms: "owned"
iam:
withOIDC: true
serviceAccounts:
- metadata:
name: alb-ingress-controller
namespace: kube-system
attachPolicy:
Version: "2012-10-17"
Statement:
- Effect: Allow
Resource: "*"
Action:
- "acm:DescribeCertificate"
- "acm:ListCertificates"
- "acm:GetCertificate"
- "ec2:AuthorizeSecurityGroupIngress"
- "ec2:CreateSecurityGroup"
- "ec2:CreateTags"
- "ec2:DeleteTags"
- "ec2:DeleteSecurityGroup"
- "ec2:DescribeAccountAttributes"
- "ec2:DescribeAddresses"
- "ec2:DescribeInstances"
- "ec2:DescribeInstanceStatus"
- "ec2:DescribeInternetGateways"
- "ec2:DescribeNetworkInterfaces"
- "ec2:DescribeSecurityGroups"
- "ec2:DescribeSubnets"
- "ec2:DescribeTags"
- "ec2:DescribeVpcs"
- "ec2:ModifyInstanceAttribute"
- "ec2:ModifyNetworkInterfaceAttribute"
- "ec2:RevokeSecurityGroupIngress"
- "elasticloadbalancing:AddListenerCertificates"
- "elasticloadbalancing:AddTags"
- "elasticloadbalancing:CreateListener"
- "elasticloadbalancing:CreateLoadBalancer"
- "elasticloadbalancing:CreateRule"
- "elasticloadbalancing:CreateTargetGroup"
- "elasticloadbalancing:DeleteListener"
- "elasticloadbalancing:DeleteLoadBalancer"
- "elasticloadbalancing:DeleteRule"
- "elasticloadbalancing:DeleteTargetGroup"
- "elasticloadbalancing:DeregisterTargets"
- "elasticloadbalancing:DescribeListenerCertificates"
- "elasticloadbalancing:DescribeListeners"
- "elasticloadbalancing:DescribeLoadBalancers"
- "elasticloadbalancing:DescribeLoadBalancerAttributes"
- "elasticloadbalancing:DescribeRules"
- "elasticloadbalancing:DescribeSSLPolicies"
- "elasticloadbalancing:DescribeTags"
- "elasticloadbalancing:DescribeTargetGroups"
- "elasticloadbalancing:DescribeTargetGroupAttributes"
- "elasticloadbalancing:DescribeTargetHealth"
- "elasticloadbalancing:ModifyListener"
- "elasticloadbalancing:ModifyLoadBalancerAttributes"
- "elasticloadbalancing:ModifyRule"
- "elasticloadbalancing:ModifyTargetGroup"
- "elasticloadbalancing:ModifyTargetGroupAttributes"
- "elasticloadbalancing:RegisterTargets"
- "elasticloadbalancing:RemoveListenerCertificates"
- "elasticloadbalancing:RemoveTags"
- "elasticloadbalancing:SetIpAddressType"
- "elasticloadbalancing:SetSecurityGroups"
- "elasticloadbalancing:SetSubnets"
- "elasticloadbalancing:SetWebACL"
- "iam:CreateServiceLinkedRole"
- "iam:GetServerCertificate"
- "iam:ListServerCertificates"
- "waf-regional:GetWebACLForResource"
- "waf-regional:GetWebACL"
- "waf-regional:AssociateWebACL"
- "waf-regional:DisassociateWebACL"
- "tag:GetResources"
- "tag:TagResources"
- "waf:GetWebACL"
You should recognize the IAM policy for the ALB Ingress Controller from my previous post. Next, we will deploy this configuration.
eksctl create cluster ./cluster.yml
Once this is created, we will need to provision an Aurora MySQL Database and EFS File System. In the console, navigate to the RDS console and create a database. Ensure, you create a security with 3306 open to the cluster nodes (this can be done via a CIDR range or the Security Group created for the node group). Then navigate to the EFS console, and create a mount point in the same VPC. Again, ensure you create a Security Group that allows NFS
traffic from the node group.
Now we will deploy the ALB Ingress Controller. Make sure you replace <YOUR VPC ID>
and <CLUSTER NAME>
specified in the cluster.yml
.
# ./specs/alb-ingress-controller.yml
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
labels:
app.kubernetes.io/name: alb-ingress-controller
name: alb-ingress-controller
rules:
- apiGroups:
- ""
- extensions
resources:
- configmaps
- endpoints
- events
- ingresses
- ingresses/status
- services
verbs:
- create
- get
- list
- update
- watch
- patch
- apiGroups:
- ""
- extensions
resources:
- nodes
- pods
- secrets
- services
- namespaces
verbs:
- get
- list
- watch
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
labels:
app.kubernetes.io/name: alb-ingress-controller
name: alb-ingress-controller
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: alb-ingress-controller
subjects:
- kind: ServiceAccount
name: alb-ingress-controller
namespace: kube-system
---
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app.kubernetes.io/name: alb-ingress-controller
name: alb-ingress-controller
namespace: kube-system
spec:
selector:
matchLabels:
app.kubernetes.io/name: alb-ingress-controller
template:
metadata:
labels:
app.kubernetes.io/name: alb-ingress-controller
spec:
containers:
- name: alb-ingress-controller
args:
- --ingress-class=alb
- --cluster-name=<CLUSTER NAME>
- --aws-vpc-id=<YOUR VPC ID>
- --aws-region=us-east-2
image: docker.io/amazon/aws-alb-ingress-controller:v1.1.5
serviceAccountName: alb-ingress-controller
Then apply the configuration via:
kubectl apply -f ./specs/alb-ingress-controller.yml
Next, we need to deploy the EFS CSI Driver so that our containers can mount our EFS share. To deploy the driver run the following command:
kubectl apply -k "github.com/kubernetes-sigs/aws-efs-csi-driver/deploy/kubernetes/overlays/stable/?ref=master"
Finally, we need to deploy Wordpress. Below is the specification file we will use to deploy it. We will walk through the components of the file further down this document. Ensure you replace <FILE SYSTEM ID>
with the ID of your EFS share and update the database configuration with the credentials you used to create the database.
# ./specs/wordpress.yml
apiVersion: v1
kind: PersistentVolume
metadata:
name: wordpress-efs-pv
spec:
capacity:
storage: 100Gi
volumeMode: Filesystem
accessModes:
- ReadWriteMany
persistentVolumeReclaimPolicy: Retain
storageClassName: efs-sc
csi:
driver: efs.csi.aws.com
volumeHandle: fs-1234567 # this should be your FS ID for EFS
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: wordpress-efs-uploads-pvc
spec:
accessModes:
- ReadWriteMany
storageClassName: efs-sc
resources:
requests:
storage: 25Gi
---
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: wordpress-ingress
annotations:
kubernetes.io/ingress.class: "alb"
alb.ingress.kubernetes.io/scheme: "internet-facing"
alb.ingress.kubernetes.io/healthcheck-path: "/index.php"
alb.ingress.kubernetes.io/success-codes: "200,201,302"
labels:
app: wordpress-ingress
spec:
rules:
- http:
paths:
- path: /*
backend:
serviceName: wordpress-service
servicePort: 80
---
apiVersion: v1
kind: Service
metadata:
name: wordpress-service
spec:
ports:
- port: 80
targetPort: 80
protocol: TCP
type: NodePort
selector:
app: wordpress
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: wordpress-deployment
labels:
app: wordpress
spec:
replicas: 3
selector:
matchLabels:
app: wordpress
template:
metadata:
labels:
app: wordpress
spec:
containers:
- image: wordpress:5.3-apache
name: wordpress
resources:
requests:
memory: 256Mi
cpu: 250m
limits:
memory: 512Mi
cpu: 500m
env:
- name: PHP_MAX_POST_SIZE
value: 1024M
- name: WORDPRESS_DB_HOST
valueFrom:
secretKeyRef:
name: wordpress-db
key: host
- name: WORDPRESS_DB_NAME
valueFrom:
secretKeyRef:
name: wordpress-db
key: name
- name: WORDPRESS_DB_USER
valueFrom:
secretKeyRef:
name: wordpress-db
key: username
- name: WORDPRESS_DB_PASSWORD
valueFrom:
secretKeyRef:
name: wordpress-db
key: password
- name: WORDPRESS_AUTH_KEY
valueFrom:
secretKeyRef:
name: wordpress-salts
key: auth_key
- name: WORDPRESS_SECURE_AUTH_KEY
valueFrom:
secretKeyRef:
name: wordpress-salts
key: secure_auth_key
- name: WORDPRESS_LOGGED_IN_KEY
valueFrom:
secretKeyRef:
name: wordpress-salts
key: logged_in_key
- name: WORDPRESS_NONCE_KEY
valueFrom:
secretKeyRef:
name: wordpress-salts
key: nonce_key
- name: WORDPRESS_AUTH_SALT
valueFrom:
secretKeyRef:
name: wordpress-salts
key: auth_salt
- name: WORDPRESS_SECURE_AUTH_SALT
valueFrom:
secretKeyRef:
name: wordpress-salts
key: secure_auth_salt
- name: WORDPRESS_LOGGED_IN_SALT
valueFrom:
secretKeyRef:
name: wordpress-salts
key: logged_in_salt
- name: WORDPRESS_NONCE_SALT
valueFrom:
secretKeyRef:
name: wordpress-salts
key: nonce_salt
ports:
- containerPort: 80
volumeMounts:
- name: wordpress-efs-uploads
mountPath: "/var/www/html/wp-content/uploads"
volumes:
- name: wordpress-efs-uploads
persistentVolumeClaim:
claimName: wordpress-efs-uploads-pvc
Breaking this file down:
- PersistentVolume - The Persistent Volume defines our EFS mount and registers it with the CSI driver.
- PersistentVolumeClaim - This is what the pods will use to mount the filesystem into the container. Notice this is defined as
ReadWriteMany
. This will enable this be mounted to multiple pods. - Service - Used to expose a NodePort on the instances so the Load Balancer can route traffic.
- Ingress - Defines the ALB configuration for the ALB Ingress Controller. This will automatically wire up the LB with the NodePort. Notice we define a few annontations to make the ALB public, the health check path, and allow mutliple status codes so that the health check will pass. Wordpress likes to redirect traffic. The 302 will timeout if the server is down.
- Deployment - The deployment is where we will mount our Volume into the container at
/wp-content/uploads
. All Wordpress code will be contained within the container while our dynamic content sits on the share. We are using two Kubernetes secrets for storing the database and salt configuration. The full documentation can be found here on how to create those secrets.
kubectl apply -f ./specs/wordpress.yml
Now that our containers are deployed, you should be able to fetch the ALB url via kubectl get ingress
. Then navigate to the URL. You should see the installation screen.
Conclusion
Now you can deploy Wordpress inside a container, with shared storage for dyanmic content, and manage patching via traditional container mechanisms. Plugins and Themes can still be managed from the WP Admin. I recommend disabling automatic updates of Wordpress and manage them entirely by building and deploying new containers. This will allow you to test new versions with your content and themes ahead of time.
Further Improvement
- Install the S3 Plugin for Wordpress so that all content such as images are pushed to S3 and delivered via Cloudfront for optimal perforamnce
- Automate the deployment of new versions of Wordpress via a CI/CD pipeline.