Moodle HA on AWS User Guide
Overview
This product deploys Moodle as a production, highly available environment using AWS CloudFormation. Instead of a single instance, it creates a complete multi-tier stack in its own VPC across two Availability Zones, so the platform tolerates the loss of a node or an Availability Zone and scales the web tier to demand.
The CloudFormation template reuses the cloudimg Moodle Amazon Machine Image as its web node. On launch, a bootstrap script makes that image stateless: it mounts shared storage, points Moodle at the managed database and cache, performs a one-time schema install, and configures the application to run behind the load balancer. No image of your own needs to be built.
What the stack creates:
- AWS WAF with AWS managed rules, attached as a Web ACL on the load balancer (optional).
- Application Load Balancer spanning two Availability Zones, terminating HTTPS.
- Auto Scaling group of stateless Moodle web nodes, plus a single dedicated cron node for Moodle scheduled tasks.
- Aurora Serverless v2 (PostgreSQL) database, with optional Multi-AZ.
- Amazon EFS for the Moodle data directory (shared course files), Regional or One Zone.
- ElastiCache Serverless for Valkey for sessions and cache, so any web node can serve any user.
- AWS Secrets Manager for the database and administrator credentials, NAT for private egress, and least-privilege IAM roles.
Architecture

Traffic flows from learners through AWS WAF and the Application Load Balancer to the Auto Scaling web tier. Every web node is stateless: sessions and cache live in Valkey, uploaded files live on EFS, and all course data lives in Aurora, so the load balancer can route any request to any node and the Auto Scaling group can replace an unhealthy node automatically. The VPC is split into public subnets (load balancer, NAT), private application subnets (web and cron nodes), and isolated data subnets (database, EFS mount targets, cache), each across both Availability Zones.
Prerequisites
- An AWS account and a subscription to this product in AWS Marketplace.
- An EC2 key pair in the deployment Region (for administrative SSH access to the web nodes).
- The public IP address you will administer from, as a CIDR (for example
203.0.113.4/32). The template never opens SSH to the whole internet. - For production HTTPS: an AWS Certificate Manager certificate in the deployment Region, and a domain name you control.
Step 1: Deploy from the AWS Marketplace console
- Open the product in AWS Marketplace and choose Continue to Subscribe, then Continue to Configuration.
- Choose the Moodle HA CloudFormation delivery option and your Region, then Continue to Launch.
- Choose Launch CloudFormation. The Marketplace fills in the Moodle AMI for the
ImageIdparameter automatically. - Complete the parameters (see the table below), acknowledge that the stack creates IAM resources, and create the stack.
- Wait for the stack to reach CREATE_COMPLETE, then allow a few minutes for the web nodes to finish bootstrapping and become healthy behind the load balancer.
Step 2: Deploy from the AWS CLI
After subscribing, you can deploy the template directly. Supply the subscribed Moodle AMI id for ImageId (shown in the Marketplace launch page):
aws cloudformation create-stack \
--stack-name moodle-ha \
--template-url https://cloudimg-website-prod.s3.amazonaws.com/marketplace/cfn/moodle-ha-aws/moodle-ha.yaml \
--capabilities CAPABILITY_IAM \
--parameters \
ParameterKey=ImageId,ParameterValue=ami-0example1234567890 \
ParameterKey=KeyName,ParameterValue=my-key \
ParameterKey=AvailabilityZone1,ParameterValue=us-east-1a \
ParameterKey=AvailabilityZone2,ParameterValue=us-east-1b \
ParameterKey=AdminCidr,ParameterValue=203.0.113.4/32 \
ParameterKey=CertificateArn,ParameterValue=arn:aws:acm:us-east-1:111122223333:certificate/abcde \
ParameterKey=DomainName,ParameterValue=moodle.example.com
Watch progress with:
aws cloudformation describe-stacks --stack-name <stack-name> --region <region> \
--query 'Stacks[0].StackStatus' --output text
Stack parameters
| Parameter | Purpose | Notes |
|---|---|---|
ImageId |
The Moodle web AMI | Injected automatically by AWS Marketplace |
InstanceType |
EC2 type for web and cron nodes | Default m5.large |
KeyName |
EC2 key pair for SSH admin | Required |
VpcCidr |
CIDR for the new VPC | Default 10.0.0.0/16 |
AvailabilityZone1 / AvailabilityZone2 |
The two AZs for the layout | Pick two distinct AZs |
AdminCidr |
CIDR allowed to SSH to the nodes | Your IP as a /32; never 0.0.0.0/0 |
CertificateArn |
ACM certificate for HTTPS | Leave blank for an HTTP-only test |
DomainName |
Site domain | Optional; otherwise the load balancer DNS name is used |
EnableWAF |
Attach AWS WAF managed rules | yes or no (default yes) |
MinSize / MaxSize / DesiredCapacity |
Auto Scaling web tier sizing | Default 2 / 4 / 2 |
DbMultiAz |
Aurora writer plus reader across two AZs | yes for HA, no for cost |
DbMinAcu / DbMaxAcu |
Aurora Serverless v2 capacity range | Default 0.5 to 4 ACUs |
EfsHighAvailability |
Regional (multi-AZ) vs One Zone EFS | yes for HA, no for cost |
MoodleAdminEmail |
Site administrator email |
First access
Confirm the stack finished and read its Outputs:
aws cloudformation describe-stacks --stack-name <stack-name> --region <region> \
--query 'Stacks[0].StackStatus' --output text | grep -qx CREATE_COMPLETE && echo "stack CREATE_COMPLETE"
aws cloudformation describe-stacks --stack-name <stack-name> --region <region> \
--query 'Stacks[0].Outputs' --output table
- MoodleURL is the site address (your domain, or the load balancer DNS name).
- AlbDnsName is the load balancer DNS name to point your domain at.
- DbSecretArn is the AWS Secrets Manager secret holding the database and administrator credentials.
First wait until every web node is healthy behind the load balancer (the one-time schema install finishes shortly after the stack completes, so this polls). This is the API-truth readiness gate and also catches a node that deployed but cannot serve — for example a storage mount that did not attach — which a single page request would otherwise hide:
for i in $(seq 1 96); do states=$(aws elbv2 describe-target-health --target-group-arn "<TargetGroupArn>" --region <region> --query 'TargetHealthDescriptions[].TargetHealth.State' --output text); healthy=$(printf '%s' "$states" | tr '[:space:]' '\n' | grep -c '^healthy$'); other=$(printf '%s' "$states" | tr '[:space:]' '\n' | grep -cv '^healthy$'); echo "targets healthy=$healthy other=$other"; [ "$healthy" -ge 1 ] && [ "$other" -eq 0 ] && break; sleep 15; done; [ "$healthy" -ge 1 ] && [ "$other" -eq 0 ]
Now confirm the load balancer actually serves the Moodle sign-in page. Once you have pointed your DNS at AlbDnsName the --resolve flags are unnecessary — they let you verify immediately, before DNS propagates, by pinning your site host to the load balancer (across all of its IPs):
url="<MoodleURL>"; host=$(printf '%s' "$url" | sed -E 's#^https?://##; s#/.*##'); albdns="<AlbDnsName>"
for i in $(seq 1 96); do res=""; if [ "$host" != "$albdns" ]; then albips=$(dig +short "$albdns" | grep -E '^[0-9.]+$' | paste -sd, -); [ -n "$albips" ] && res="--resolve $host:443:$albips --resolve $host:80:$albips --resolve $albdns:443:$albips --resolve $albdns:80:$albips"; fi; code=$(curl -fsS -o /dev/null -w '%{http_code}' -L $res "$url/login/index.php" 2>/dev/null || echo 000); [ "$code" = "200" ] && { echo "login page HTTP 200 after ~$((i*15))s"; break; }; sleep 15; done; [ "$code" = "200" ]
The administrator password is generated during the one-time schema install and stored in Secrets Manager, never shipped in the image. Retrieve it (and confirm both credential keys are present — the install writes them shortly after the site comes up, so this polls):
for i in $(seq 1 24); do out=$(aws secretsmanager get-secret-value --secret-id "<DbSecretArn>" --region <region> --query SecretString --output text 2>/dev/null); printf '%s' "$out" | python3 -c 'import sys,json; d=json.load(sys.stdin); assert d.get("moodle_admin_user") and d.get("moodle_admin_pass")' 2>/dev/null && break; sleep 15; done; printf '%s' "$out" | python3 -c 'import sys,json; d=json.load(sys.stdin); assert d.get("moodle_admin_user") and d.get("moodle_admin_pass"); print("admin credentials present for", d["moodle_admin_user"])'
Open MoodleURL in a browser and sign in as admin with that password.
High availability behaviour and toggles
- Web tier — the Auto Scaling group runs across both Availability Zones behind the load balancer. If a node fails its health check, the group replaces it; because web nodes are stateless, no user data is lost.
DbMultiAz—yesruns an Aurora Serverless v2 writer plus a reader in a second Availability Zone;noruns a single instance for lower cost.EfsHighAvailability—yesuses Regional EFS with a mount target in each Availability Zone;nouses One Zone EFS for lower cost.EnableWAF— attaches AWS managed rule groups to the load balancer to filter common web exploits.
Sessions and cache run on ElastiCache Serverless for Valkey, which is multi-AZ by design, so a user signed in on one node continues seamlessly on another.
Scaling and cron
Adjust DesiredCapacity, MinSize, and MaxSize to size the web tier, or attach Auto Scaling policies to scale on load. A single dedicated cron node runs moodle-cron.timer; the web nodes have the cron timer disabled so scheduled tasks run exactly once per minute regardless of how many web nodes are in service.
HTTPS and a custom domain
For production, supply CertificateArn (an ACM certificate in the deployment Region) and DomainName. The load balancer then serves HTTPS and redirects HTTP, and Moodle is configured with the matching site address and proxy settings. Create a DNS record pointing your domain at the AlbDnsName output.
Security
- Deployed in a dedicated VPC with public, private application, and isolated data subnets across two Availability Zones.
- SSH is restricted to your
AdminCidr; the database, EFS, and cache accept traffic only from the web tier security group. - Database and administrator credentials are generated at deploy time and stored in AWS Secrets Manager.
- Least-privilege IAM instance roles (AWS Systems Manager core only); no long-lived keys; IMDSv2 is enforced.
- AWS WAF managed rules protect the public endpoint when enabled.
Backups and maintenance
- Aurora takes automated backups and you can take manual snapshots at any time; EFS stores data redundantly within the Region.
- To move to a newer Moodle AMI version, update the stack with the new
ImageId; the Auto Scaling group rolls the web nodes. - Application data is never stored on the web nodes, so they can be replaced freely.
Support
This product includes 24/7 technical support from cloudimg by email and chat, covering deployment, scaling, upgrades, plugin configuration, and database administration. Contact support@cloudimg.co.uk.