Developer Tools AWS

Locust 2.47.0 on AWS User Guide

| Product: Locust 2.47.0 on Ubuntu 24.04 on AWS

Overview

This guide covers launching and using the Locust 2.47.0 on Ubuntu 24.04 AMI from cloudimg on Amazon EC2. It is one version of the cloudimg developer and API tools listing on AWS Marketplace; you pick it as the software version when you subscribe and launch.

Locust is an open source load testing tool. You describe how a user behaves in a short Python file (a locustfile), choose how many simulated users to run and the address of the system to test, and Locust sends that traffic while its web UI shows requests per second, response time percentiles and every failure live. Tests can be started, changed and stopped from the browser or from scripts, and the results downloaded as CSV or an HTML report.

The cloudimg image locks Locust down:

  • HTTPS from the first boot. nginx serves Locust on port 443 with a certificate generated on your instance's first boot. Port 80 redirects to HTTPS.
  • A password unique to your instance, on everything. Locust's own web UI has no login at all, and anyone who can open it can send load at any address. On this image HTTP Basic Auth is required for the user admin on every page, chart, download and API call. The password is generated on first boot and written to /var/lib/cloudimg/locust-credentials.txt (root only); nginx stores only a bcrypt hash of it. No password is baked into the image.
  • Locust itself is not on the network. The Locust web server listens on 127.0.0.1 only and runs on its own, with no distributed master or worker port open. The security group needs only 22, 80 and 443.
  • Another web site cannot start, stop or change your tests. Every action that changes a test (start, stop, reset, edit users) is a POST request in Locust 2.47.0, and nginx refuses any POST whose browser Origin is not this instance, so a page you visit while signed in cannot use your login to start a test. Locust's open CORS headers are removed so no other site can read the results.
  • Load tests cannot reach the instance metadata service. You choose the address a test targets, but Locust and nginx are blocked at the kernel level from the EC2 instance metadata service (IPv4 and IPv6), so a test can never read the instance's metadata or role credentials.
  • The test code is protected. The example locustfile /etc/locust/locustfile.py and the configuration file are owned by root. The Locust service runs as its own unprivileged user that cannot change them, so only an administrator decides what code it runs.
  • A pinned, audited install. Locust 2.47.0 and every Python package it uses (39 in total, pip included) are installed from a lock file that pins each package's exact version and checksum, and pip-audit finds no known vulnerability in that set.
  • Hardened defaults. Locust runs with no capabilities, a read only view of the system except its own state directory, and a memory ceiling. nginx and Locust refuse to start until first boot has generated the certificate and the password, nginx drops requests for host names the instance does not serve, and the AMI requires IMDSv2.
  • Fully patched Ubuntu 24.04 LTS at build time, with unattended security upgrades switched on.

What is on the image: Locust 2.47.0 (in the virtual environment /opt/locust/venv, with its web UI), an example locustfile, nginx, Ubuntu 24.04 LTS with Python 3.12, cloud-init and the AWS Systems Manager agent.

Default user: ubuntu · Architecture: x86_64 · Recommended instance type: m5.large

Prerequisites

  • An AWS account subscribed to the listing, an EC2 key pair and a VPC subnet with a route to the internet (or a private route you use for management).
  • A security group that allows TCP 22 (SSH), TCP 443 (Locust) and TCP 80 (redirects to HTTPS) from your own networks only.
  • Permission to load test the system you point Locust at. Only test systems you own or are authorised to test.

Step 1: Launch the instance

In the AWS Marketplace listing choose Continue to Subscribe, then Continue to Configuration, pick Locust 2.47.0 on Ubuntu 24.04 as the software version and your Region, then Launch through EC2. Choose m5.large (one Locust process uses about one CPU core; choose a larger instance for heavier tests, Step 10), your key pair, a security group as above, and a subnet that can reach the system you want to test.

With the AWS CLI (replace the values in angle brackets):

aws ec2 run-instances --region <region> --image-id <ami-id-from-the-listing> \
  --instance-type m5.large --key-name <your-key-pair> \
  --security-group-ids <your-security-group> --subnet-id <your-subnet> \
  --metadata-options HttpTokens=required,HttpEndpoint=enabled \
  --tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=locust}]'

Step 2: Connect to your instance

Version SSH user
Locust 2.47.0 on Ubuntu 24.04 ubuntu
ssh -i <your-key.pem> ubuntu@<public-ip>

Step 3: Check the services

First boot generates the certificate and the password, then Locust and nginx start:

systemctl is-active locust nginx locust-firstboot
sudo test -f /var/lib/cloudimg/locust-firstboot.done && echo FIRSTBOOT_DONE

All three units are active and the first boot marker is present:

active
active
active
FIRSTBOOT_DONE

Only SSH and nginx listen on the network. Locust's web server (8089) listens on 127.0.0.1 only:

sudo ss -lntp | awk 'NR>1 {print $4, $6}' | grep -v '127.0.0.5' | sed -E 's/,pid=[0-9]+,fd=[0-9]+//g' | sort
0.0.0.0:22 users:(("sshd"),("systemd"))
0.0.0.0:443 users:(("nginx"),("nginx"),("nginx"))
0.0.0.0:80 users:(("nginx"),("nginx"),("nginx"))
127.0.0.1:8089 users:(("locust"))
[::]:22 users:(("sshd"),("systemd"))
[::]:443 users:(("nginx"),("nginx"),("nginx"))
[::]:80 users:(("nginx"),("nginx"),("nginx"))

Step 4: Get your URL and password

sudo cat /var/lib/cloudimg/locust-credentials.txt

The file is readable by root only and holds the URL, the user name and this instance's password:

# Locust 2.47.0 - generated on first boot by locust-firstboot.service
# These credentials are unique to this instance; none was baked into the image. Only a bcrypt hash
# of the password is stored (nginx). Keep this file secret.

LOCUST_URL=https://ec2-<public-ip-dashed>.<region>.compute.amazonaws.com/
LOCUST_USERNAME=admin
LOCUST_PASSWORD=<LOCUST_PASSWORD>
#
# Open LOCUST_URL in a browser (self-signed certificate minted on this instance's first boot;
# port 80 redirects to https) and sign in with the username and password above. The same
# credential protects every page and the JSON API.
# Load test scenarios: /etc/locust/locustfile.py (edit with sudo, then: sudo systemctl restart locust)
# Self-test (a short test against a throwaway local target):  sudo locust-selftest
# Change the name it is served as:  sudo locust-set-hostname <your-dns-name>
# User guide: https://www.cloudimg.co.uk/guides/locust-2-47-0-on-ubuntu-24-04-aws/

The URL uses the instance's public DNS name from EC2 instance metadata. The certificate also names the public and private IP addresses, so https://<public-ip>/ works too. To serve Locust under your own DNS name, see Step 12.

Step 5: Sign in

Open the LOCUST_URL in your browser. The certificate is self signed, so your browser asks you to confirm it the first time (or install your own certificate, Step 12). Sign in as admin with the password from Step 4; the browser remembers the login for the session.

Locust opens on Start new load test:

The Locust 2.47.0 start page on cloudimg: the number of users, the ramp up rate and the Host fields with a Start button

Step 6: Run your first test

Fill in the form and choose Start:

  • Number of users (peak concurrency): how many simulated users run at once, for example 20.
  • Ramp up (users started/second): how quickly Locust adds them, for example 4.
  • Host: the base address of the system under test, for example https://staging.example.com. The example locustfile requests / on this host.

The Statistics tab shows each request type with its count, failures, response time percentiles and current requests per second, updated every two seconds. The screenshot shows 20 users against a small sample web server:

Locust running a 20 user test: the statistics table shows 198 GET / requests with no failures, a median response time of 3 ms and 10 requests per second

Charts plots requests per second, failures, response times and the number of users over the run:

The Locust Charts tab: total requests per second rising to 10, the 50th and 95th percentile response times and the number of users ramping to 20

Use Edit to change the user count while the test runs, Stop to end it, Reset to clear the statistics, and Download Data for CSV files and an HTML report. Failures lists every failed request with its error.

The image also has a self test. It starts a throwaway web server on the instance, runs a short 2 user test against it through nginx with your password, reads the statistics back, stops the test, resets the statistics and checks the security settings. It refuses to run while a test of yours is running:

sudo locust-selftest
SELFTEST-OK https 301 + health + 401 noauth + 401 wrong + web UI + 2-user test on 127.0.0.1 (4 requests, 0 failures) + stop + 403 cross-origin POST + 405 GET /stop + reset + 444 unknown host

Step 7: Write your own test

The example locustfile is a starting point. It is Python that the Locust service runs, so it lives in /etc/locust, owned by root, and only an administrator can change it:

cat /etc/locust/locustfile.py
"""Example load test shipped with the cloudimg Locust image.

Each simulated user requests the root path ("/") of the Host you enter in the web UI, waiting 1-3 seconds between
requests, and counts a request that gets no answer within 10 seconds as a failure. Nothing is sent until you start a
test. Replace this file with your own scenarios:

    sudo nano /etc/locust/locustfile.py
    sudo systemctl restart locust

This file is Python that the locust service runs, so it is owned by root and only an admin can change it.
"""
from locust import HttpUser, between, task


class WebsiteUser(HttpUser):
    wait_time = between(1, 3)

    @task
    def index(self):
        self.client.get("/", name="GET /", timeout=10)

Replace it with your own scenario, then restart Locust to load it. This example logs in once per simulated user and then mixes two requests, three to one, against your own application (the Locust documentation covers tasks, wait times, tags and custom clients):

sudo tee /etc/locust/locustfile.py >/dev/null <<'EOF'
from locust import HttpUser, between, task


class ShopUser(HttpUser):
    host = "https://<your-domain>"
    wait_time = between(1, 5)

    def on_start(self):
        self.client.post("/login", json={"user": "loadtest", "password": "test-only"}, timeout=10)

    @task(3)
    def browse(self):
        self.client.get("/products", timeout=10)

    @task(1)
    def basket(self):
        self.client.get("/basket", timeout=10)
EOF
sudo systemctl restart locust

Keep a timeout on each request so that a target that does not answer shows up as a failure instead of hanging a simulated user. Settings such as the log level live in /etc/locust/locust.conf (configuration reference); edit it with sudo and restart Locust. The address Locust listens on is fixed in the service so it always stays behind nginx. Locust runs from its own Python environment; to use another Python package in your locustfile, install it there with sudo /opt/locust/venv/bin/pip install <package> and restart Locust.

Step 8: Drive Locust from scripts

Everything in the web UI is also an HTTP API on the same address and login: POST /swarm starts or changes a test (form fields user_count, spawn_rate, host and optionally run_time), POST /stop stops it, POST /stats/reset clears the statistics, and GET /stats/requests returns the live statistics as JSON. CSV files are at /stats/requests/csv and /stats/failures/csv, and the HTML report at /stats/report. A script may send no Origin header at all; a browser page must come from the instance itself.

This example starts a small web server on the instance as a stand in for your application, runs a 5 user test against it for a few seconds through the API, prints a summary and stops the test. The password is read from the credentials file and passed to curl on standard input, so it never appears in the process list:

sudo bash -c '
  F=/var/lib/cloudimg/locust-credentials.txt
  P=$(grep "^LOCUST_PASSWORD=" $F | cut -d= -f2-)
  H=$(grep "^LOCUST_URL=" $F | sed -E "s#^LOCUST_URL=https://([^/]+)/.*#\1#")
  api() { printf "user = \"admin:%s\"\n" "$P" | curl -K - -sk --resolve "$H:443:127.0.0.1" "$@"; }
  mkdir -p /run/demo-site && echo "<h1>demo</h1>" > /run/demo-site/index.html
  python3 -m http.server 8000 --bind 127.0.0.1 --directory /run/demo-site >/dev/null 2>&1 & S=$!
  sleep 1
  api -X POST -d user_count=5 -d spawn_rate=5 -d host=http://127.0.0.1:8000 -d run_time=60s "https://$H/swarm" \
    | python3 -c "import sys,json; d=json.load(sys.stdin); print(\"start:\", d[\"message\"], \"at\", d[\"host\"])"
  sleep 12
  api "https://$H/stats/requests" | python3 -c "
import sys, json
d = json.load(sys.stdin)
a = [s for s in d[\"stats\"] if s[\"name\"] == \"Aggregated\"][0]
print(\"state:\", d[\"state\"], \"| users:\", d[\"user_count\"])
print(\"requests sent:\", a[\"num_requests\"] > 0, \"| failures:\", a[\"num_failures\"])"
  api -X POST "https://$H/stop" | python3 -c "import sys,json; print(\"stop:\", json.load(sys.stdin)[\"message\"])"
  api -o /dev/null -w "reset statistics -> HTTP %{http_code}\n" -X POST "https://$H/stats/reset"
  kill $S; rm -rf /run/demo-site
'
start: Swarming started at http://127.0.0.1:8000
state: running | users: 5
requests sent: True | failures: 0
stop: Test stopped
reset statistics -> HTTP 200

From another machine, call https://<your-locust-host>/swarm and the other routes with the same user and password (for example curl -u admin, letting curl prompt for the password, or your CI tool's HTTP client with Basic Auth).

Step 9: Load tests cannot reach the instance metadata service

Locust sends traffic to whatever host you give it, so the image makes sure that can never be the instance itself: Locust and nginx are blocked from the EC2 instance metadata service (169.254.169.254 and fd00:ec2::254) and the Azure wire server address. A test pointed there sends requests that never get an answer, and each one is recorded as a connection timeout:

A Locust test against http://169.254.169.254: the Failures tab lists only ConnectTimeoutError entries and the failure rate is 100%

The same check from the command line, through the API (it takes about 25 seconds):

sudo bash -c '
  F=/var/lib/cloudimg/locust-credentials.txt
  P=$(grep "^LOCUST_PASSWORD=" $F | cut -d= -f2-)
  H=$(grep "^LOCUST_URL=" $F | sed -E "s#^LOCUST_URL=https://([^/]+)/.*#\1#")
  api() { printf "user = \"admin:%s\"\n" "$P" | curl -K - -sk --resolve "$H:443:127.0.0.1" "$@"; }
  api -o /dev/null -X POST -d user_count=2 -d spawn_rate=2 -d host=http://169.254.169.254 -d run_time=60s "https://$H/swarm"
  sleep 24
  api "https://$H/stats/requests" | python3 -c "
import sys, json, re
d = json.load(sys.stdin)
a = [s for s in d[\"stats\"] if s[\"name\"] == \"Aggregated\"][0]
print(\"requests sent:\", a[\"num_requests\"] > 0, \"| every one failed:\", a[\"num_failures\"] == a[\"num_requests\"])
e = d[\"errors\"][0][\"error\"]
print(\"each request ended with:\", e.split(\"(\")[0], \"-\", re.search(r\"Connection to [0-9.]+ timed out\", e).group(0))"
  api -o /dev/null -X POST "https://$H/stop"
  api -o /dev/null -X POST "https://$H/stats/reset"
'
requests sent: True | every one failed: True
each request ended with: ConnectTimeoutError - Connection to 169.254.169.254 timed out

The block is a kernel level rule on the Locust and nginx services (Step 11 shows it). It does not depend on the locustfile, so your own tests are covered too.

Step 10: Bigger tests

One Locust process uses one CPU core. For more load, choose a larger instance type (for example m5.xlarge or c5.2xlarge) and keep an eye on the instance's CPU while a test runs: when Locust's own CPU is near 100 percent, the response times it reports include its own delay. The Locust service has a memory ceiling of 80 percent of the instance's memory, so a very large test cannot exhaust the instance; the service restarts if it reaches it.

You can also run a test without the web UI, for example from a CI job, with locust --headless. Run it inside the same metadata block and as the locust user with systemd-run. This example runs 5 users against a stand in web server for 5 seconds and prints the summary Locust writes with --json:

sudo bash -c '
  mkdir -p /run/demo-site && echo "<h1>demo</h1>" > /run/demo-site/index.html
  python3 -m http.server 8001 --bind 127.0.0.1 --directory /run/demo-site >/dev/null 2>&1 & S=$!
  sleep 1
  systemd-run --quiet --wait --pipe --collect -p User=locust -p WorkingDirectory=/var/lib/locust \
    -p "IPAddressDeny=169.254.169.254 168.63.129.16 fd00:ec2::254" \
    /opt/locust/venv/bin/locust -f /etc/locust/locustfile.py --headless -u 5 -r 5 -t 5s \
    --host http://127.0.0.1:8001 --json 2>/dev/null \
    | python3 -c "
import sys, json
s = json.load(sys.stdin)
print(\"headless run:\", [r[\"name\"] for r in s], \"| requests sent:\", sum(r[\"num_requests\"] for r in s) > 0, \"| failures:\", sum(r[\"num_failures\"] for r in s))"
  kill $S; rm -rf /run/demo-site
'
headless run: ['GET /'] | requests sent: True | failures: 0

Locust can also spread one test over several machines (a master and workers). This image runs Locust on its own and opens no master or worker port. If you set up workers, the master's port (5557) has no authentication: allow it in the security group from your worker instances' private addresses only.

Step 11: Check the security posture from the command line

Without the password, nothing but the health check answers, and port 80 only redirects:

H=$(sudo grep '^LOCUST_URL=' /var/lib/cloudimg/locust-credentials.txt | sed -E 's#^LOCUST_URL=https://([^/]+)/.*#\1#')
R="--resolve $H:443:127.0.0.1 --resolve $H:80:127.0.0.1"
curl -s -o /dev/null -w 'http://  -> %{http_code} %{redirect_url}\n' $R "http://$H/"
curl -sk -o /dev/null -w 'web UI, no login -> HTTP %{http_code}\n' $R "https://$H/"
curl -sk -o /dev/null -w 'statistics API, no login -> HTTP %{http_code}\n' $R "https://$H/stats/requests"
curl -sk -o /dev/null -w 'start a test, no login -> HTTP %{http_code}\n' $R -X POST -d user_count=1 -d spawn_rate=1 "https://$H/swarm"
curl -sk -o /dev/null -w 'health -> HTTP %{http_code}\n' $R "https://$H/health"
curl -sk -o /dev/null -w 'unknown host -> %{http_code}\n' --resolve "evil.example:443:127.0.0.1" "https://evil.example/" || true
http://  -> 301 https://ec2-<public-ip-dashed>.<region>.compute.amazonaws.com/
web UI, no login -> HTTP 401
statistics API, no login -> HTTP 401
start a test, no login -> HTTP 401
health -> HTTP 200
unknown host -> 000

000 means nginx closed the connection without answering, which is how it treats any host name it does not serve.

With the password, a request to start or stop a test whose browser Origin is another web site is refused (HTTP 403), every route that changes a test answers only to POST (a link or an image on another site is a GET, HTTP 405), and Locust's CORS headers never reach the browser:

sudo bash -c '
  F=/var/lib/cloudimg/locust-credentials.txt
  P=$(grep "^LOCUST_PASSWORD=" $F | cut -d= -f2-)
  H=$(grep "^LOCUST_URL=" $F | sed -E "s#^LOCUST_URL=https://([^/]+)/.*#\1#")
  api() { printf "user = \"admin:%s\"\n" "$P" | curl -K - -sk --resolve "$H:443:127.0.0.1" -o /dev/null "$@"; }
  api -w "with login -> HTTP %{http_code}\n" "https://$H/"
  api -w "start a test, other origin -> HTTP %{http_code}\n" -X POST -H "Origin: https://evil.example" -d user_count=1 -d spawn_rate=1 -d host=https://example.com "https://$H/swarm"
  api -w "stop, other origin -> HTTP %{http_code}\n" -X POST -H "Origin: https://evil.example" "https://$H/stop"
  api -w "reset, same origin -> HTTP %{http_code}\n" -X POST -H "Origin: https://$H" "https://$H/stats/reset"
  for r in swarm stop stats/reset user; do api -w "GET /$r -> HTTP %{http_code}\n" "https://$H/$r"; done
  printf "user = \"admin:%s\"\n" "$P" | curl -K - -sk --resolve "$H:443:127.0.0.1" -D - -o /dev/null -H "Origin: https://evil.example" "https://$H/stats/requests" \
    | grep -ci "^access-control-" | sed "s/^/CORS headers sent: /" || true
'
with login -> HTTP 200
start a test, other origin -> HTTP 403
stop, other origin -> HTTP 403
reset, same origin -> HTTP 200
GET /swarm -> HTTP 405
GET /stop -> HTTP 405
GET /stats/reset -> HTTP 405
GET /user -> HTTP 405
CORS headers sent: 0

nginx keeps only a bcrypt hash of the password; the plain text is in the root only credentials file:

sudo sed -E 's/\$2y\$12\$.*/$2y$12$<bcrypt-hash>/' /etc/nginx/.locust-htpasswd
sudo stat -c '%a %U:%G %n' /etc/nginx/.locust-htpasswd /var/lib/cloudimg/locust-credentials.txt
admin:$2y$12$<bcrypt-hash>
640 root:www-data /etc/nginx/.locust-htpasswd
600 root:root /var/lib/cloudimg/locust-credentials.txt

The certificate was generated on this instance and names its own addresses:

H=$(sudo grep '^LOCUST_URL=' /var/lib/cloudimg/locust-credentials.txt | sed -E 's#^LOCUST_URL=https://([^/]+)/.*#\1#')
echo | openssl s_client -connect 127.0.0.1:443 -servername "$H" 2>/dev/null | openssl x509 -noout -subject -ext subjectAltName
subject=CN = ec2-<public-ip-dashed>.<region>.compute.amazonaws.com, O = cloudimg
X509v3 Subject Alternative Name: 
    DNS:ec2-<public-ip-dashed>.<region>.compute.amazonaws.com, IP Address:<public-ip>, IP Address:<private-ip>, IP Address:127.0.0.1, DNS:localhost

Locust and nginx cannot reach the instance metadata service; Locust runs as its own user with no capabilities, can write only to its state directory, listens on 127.0.0.1 only, and cannot change the test code it runs:

for u in locust nginx; do echo "$u IPAddressDeny: $(systemctl show $u -p IPAddressDeny --value | tr ' ' '\n' | sort | xargs)"; done
systemctl show locust -p User -p ProtectSystem -p ReadWritePaths -p CapabilityBoundingSet -p NoNewPrivileges
grep -E '^Memory(High|Max)=' /etc/systemd/system/locust.service
sudo tr '\0' ' ' < /proc/$(systemctl show -p MainPID --value locust)/cmdline; echo
stat -c '%a %U:%G %n' /etc/locust /etc/locust/locustfile.py /etc/locust/locust.conf
sudo -u locust test -w /etc/locust/locustfile.py && echo "locustfile writable by locust" || echo "locustfile is read only for the locust user"
locust IPAddressDeny: 168.63.129.16/32 169.254.169.254/32 fd00:ec2::254/128
nginx IPAddressDeny: 168.63.129.16/32 169.254.169.254/32 fd00:ec2::254/128
CapabilityBoundingSet=
User=locust
ReadWritePaths=/var/lib/locust
ProtectSystem=strict
NoNewPrivileges=yes
MemoryHigh=70%
MemoryMax=80%
/opt/locust/venv/bin/python /opt/locust/venv/bin/locust --config /etc/locust/locust.conf --web-host 127.0.0.1 --web-port 8089 
755 root:root /etc/locust
644 root:root /etc/locust/locustfile.py
644 root:root /etc/locust/locust.conf
locustfile is read only for the locust user

Step 12: Your own DNS name and certificate

Point a DNS record at the instance, then set the name Locust is served as. This mints a new self signed certificate for the name, updates nginx, the cross site protection and the URL in the credentials file, and keeps the name across reboots:

sudo locust-set-hostname <your-domain>

To use a certificate from your own certificate authority instead, copy it over the generated pair and reload nginx (running locust-set-hostname again replaces it with a self signed one):

sudo install -m 0644 <your-domain>.crt /etc/ssl/locust/locust.crt
sudo install -m 0600 <your-domain>.key /etc/ssl/locust/locust.key
sudo nginx -t && sudo systemctl reload nginx

Step 13: Change the password or add users

The login is an nginx Basic Auth file. To set a new password for admin, hand it to htpasswd on standard input (printf is a shell builtin, so the password never appears in the process list; start the line with a space so most shells keep it out of your history), then reload nginx:

 printf '%s\n' '<new-password>' | sudo htpasswd -i -B -C 12 /etc/nginx/.locust-htpasswd admin
sudo systemctl reload nginx

Running sudo htpasswd -B -C 12 /etc/nginx/.locust-htpasswd <user> without -i prompts for the password instead. Use it with another user name to give a colleague or a CI system its own login, and sudo htpasswd -D /etc/nginx/.locust-htpasswd <user> to remove one. Then update or delete LOCUST_PASSWORD in /var/lib/cloudimg/locust-credentials.txt so it does not hold an old password.

nginx checks the login and does not pass it on to Locust.

Step 14: Server components

Component Path
Locust (Python virtual environment) /opt/locust/venv (/opt/locust/venv/bin/locust)
Locustfile and configuration /etc/locust/locustfile.py, /etc/locust/locust.conf (root owned)
Locust service locust.service (user locust; web UI 127.0.0.1:8089; state /var/lib/locust)
Package lock, pip-audit result, licence, build record /usr/share/doc/cloudimg-locust/
nginx site /etc/nginx/sites-available/cloudimg-locust
Basic Auth file /etc/nginx/.locust-htpasswd (bcrypt)
Certificate and key /etc/ssl/locust/
First boot script /usr/local/sbin/locust-firstboot.sh
Host name helper /usr/local/sbin/locust-set-hostname
Self test /usr/local/sbin/locust-selftest
Credentials /var/lib/cloudimg/locust-credentials.txt (mode 0600)
/opt/locust/venv/bin/locust --version
nginx -v 2>&1
grep -c -- '--hash=sha256:' /usr/share/doc/cloudimg-locust/requirements.lock
sed -n '1,6p' /usr/share/doc/cloudimg-locust/BUILD-INFO.txt
locust 2.47.0 from /opt/locust/venv/lib/python3.12/site-packages/locust (Python 3.12.3)
nginx version: nginx/1.24.0 (Ubuntu)
39
locust            2.47.0 (PyPI; tag 2.47.0, commit 66279c936b4b69a8a7df3ac3b11f7e47d20780de), MIT
                  wheel sha256 a773af200bb93b1bed0b99c48c18f11343ed159803a8af3ee3ad0696b05b7d09
                  installed into /opt/locust/venv (CPython 3.12) from requirements.lock: 39 wheels, every one pinned by
                  version and sha256 (pip install --require-hashes --no-deps --only-binary :all:)
pip-audit         2.10.1 (build-only, not shipped) on the exact lock: OK 39 packages, 0 known vulnerabilities (pip-audit.json)
web UI            the compiled static bundle shipped in the wheel (locust/webui/dist: index.html, src-Cl6CiemE.js

/usr/share/doc/cloudimg-locust/requirements.lock lists every installed package with its exact version and checksum, and pip-audit.json holds the audit of that set.

Step 15: Security notes and recommendations

  • Allow ports 22, 80 and 443 in the security group from your own networks only. Anyone with the Locust password can send load from this instance to any address it can reach, so treat the password like a production credential and rotate it if it may have been shared (Step 13).
  • Only load test systems you own or are authorised to test, and agree the test window with the people who run the target. A load test from an EC2 instance to a public address is also subject to the AWS Acceptable Use Policy and the target's own terms.
  • The locustfile is Python code with the Locust service's permissions on the instance (its own user, no capabilities, no access to the metadata service). Review any locustfile before you install it.
  • Use your own DNS name and a certificate your browsers trust (Step 12).
  • Locust's web UI is a compiled bundle shipped inside the Locust release. Its upstream build tools carry advisories (for example in vite and brace-expansion), but those tools are not part of the files the instance serves. cloudimg rechecks the bundle and every Python package at each Locust release.
  • Keep the operating system patched; unattended security upgrades are on, and sudo apt-get update && sudo apt-get upgrade applies everything else.

Support and licensing

Locust is open source under the MIT licence (Carl Byström, Jonatan Heyman, Lars Holmberg and contributors); the licence text is in /usr/share/doc/cloudimg-locust/. Its Python dependencies and web UI are open source under their own permissive licences. There is no per user or per CPU licence fee. The AMI price covers cloudimg's build, hardening, first boot setup and support.

  • Email: support@cloudimg.co.uk
  • Website: https://www.cloudimg.co.uk
  • Support: 24/7, with a 24 hour response SLA