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.