
Standardizing the fleet: automated updates for multi-site management
TL;DR
|
There's a specific kind of update that Drupal agencies and enterprise teams dread: a security release in something the whole fleet runs on, the PHP runtime, the database engine, or a shared service, with a patched version available now and a deadline attached. For a team managing a single site, moving to the patched version is an afternoon of work. For a team managing 40 Drupal installations across a client portfolio or subsidiary structure, it's a race against a deadline with 40 manual deployments between them and done.
By the end of the day, most sites are patched. A few aren't, because something went wrong partway through and got deprioritized. Those few sites sit in the queue, unpatched, for longer than anyone intended.
This is the fleet management problem. It's not about any single site. It's about what happens when you multiply the overhead of managing one site by the number of sites in the fleet. It isn't specific to Drupal, either: any fleet of sites running the same stack hits the same wall, whether that's Symfony, WordPress, or a Node.js frontend. Drupal is just the running example here.
Key takeaway: Manual updates are fine for one site. For a fleet, the labor scales linearly with the number of sites, while the risk of inconsistency compounds. Any process that requires a human to repeat the same action 40 times will eventually produce a site that was missed.
The standard approach to managing a fleet of framework installations is essentially the same process as managing a single installation, repeated. Log into each site, apply the update, verify the deployment, and move to the next one. For a fleet of ten, this is tedious. For a fleet of fifty, it occupies someone for days. For a fleet of two hundred, it's a dedicated role.
The repetition isn't just expensive, it's error-prone in a particular way. Manual processes applied at scale produce inconsistent results, not because anyone is careless, but because humans can't apply the exact same procedure identically across dozens of environments. One site gets a slightly different configuration. Another gets skipped during a deployment that failed and wasn't retried. A third gets updated with a different PHP version because the person doing it didn't check the config file.
The fleet starts identically. Over time, it diverges. Sites that were launched from the same template six months ago are running different PHP versions, different service versions, and different infrastructure configurations. The divergence is invisible until something breaks, at which point the debugging process has to account for the fact that the site that's broken may not match the site that's working.
Key takeaway: Version drift across a fleet isn't just a maintenance problem. It's a security surface and a debugging liability. A fleet where every site is running the same configuration is fundamentally easier to secure and support than one where every site is slightly different.
Version drift tends to be treated as a cosmetic problem, something to clean up eventually, when there's time. It's actually a security and operational liability that gets more expensive the longer it persists.
From a security perspective, a fleet with inconsistent patch levels has an inconsistent attack surface. A vulnerability that's been patched on 38 of your 40 Drupal sites is still an open vulnerability, because the two unpatched sites are real targets. Fleet security is only as strong as the least-patched site.
From a support perspective, a fleet where every site is slightly different means that a solution that works for one site may not work for another. Debugging a reported issue requires first establishing what's actually running on that specific site, which may differ from what you think is running, because the config file was updated six months ago but this particular site wasn't included in that deployment batch.
The only sustainable answer to version drift is making it structurally unnecessary: not by being more disciplined about manual updates, but by making manual updates unnecessary in the first place.
Key takeaway: Fleet automation isn't a custom tooling project. It's using the platform API and a version-controlled config file together: the config defines what every site should be running, and the API applies that definition across the fleet without site-by-site intervention.
Upsun is a platform-as-a-service that manages the infrastructure layer of your application stack so your team doesn't have to. For fleet management, that means combining a shared config file with the Upsun API to propagate changes across every site in the fleet automatically.
The config file defines the target state for every site. A Drupal fleet config might look like this:
applications:
drupal:
type: php:8.3 # change this once to update the whole fleet
variables:
env:
DRUPAL_HASH_SALT: "change-per-site" # overridden at the project level per site
relationships:
database:
service: db
endpoint: mysql
cache:
service: redis
endpoint: redis
services:
db:
type: mariadb:10.11 # platform manages patching within this version
redis:
type: redis:7.2
When PHP 8.3 needs to move to 8.4, or MariaDB needs a version bump, that change happens in the config file. One change, one commit, one code review. Then a script iterates over every project in the fleet and triggers a deployment:
import requests
API_TOKEN = "your-api-token"
FLEET_PROJECT_IDS = ["project-1", "project-2", "project-3"] # or fetched dynamically via API
# Exchange the API token for a short-lived access token (valid ~15 minutes)
auth = requests.post(
"https://auth.upsun.com/oauth2/token",
auth=("platform-api-user", ""),
data={"grant_type": "api_token", "api_token": API_TOKEN},
)
access_token = auth.json()["access_token"]
for project_id in FLEET_PROJECT_IDS:
response = requests.post(
f"https://api.upsun.com/projects/{project_id}/environments/main/redeploy",
headers={"Authorization": f"Bearer {access_token}"},
)
if response.status_code in (200, 201, 202):
print(f"{project_id}: redeploy triggered")
else:
print(f"{project_id}: redeploy failed - {response.status_code}")
The script doesn't configure anything. The config file already defines the target state. The script just tells each project to apply it. Each redeploy is triggered independently, so one project doesn't wait on another, and the output gives you a clear record of which projects succeeded and which need attention.
This is what fleet management looks like when it's not a manual process. The update cycle is: change the config file, commit it, and run the script. The human involvement is in the decision about what to change, not in applying the change 40 times.
Key takeaway: A fleet where every site is known to be running exactly what the config file says removes an entire category of uncertainty from operations, support, and security. You stop asking "what is this site running?" and start knowing.
The operational benefits of a consistent fleet go beyond the time saved on updates.
Support becomes faster because every site in the fleet is a known configuration. When a client reports an issue, you're debugging the application, not the environment. The environment is the same as every other environment in the fleet.
Security becomes auditable. "All sites are running PHP 8.3.x with the MariaDB 10.11 service layer" is a statement you can make and verify, because the config file is version-controlled and the API deployment logs confirm which projects are running which version of it.
Onboarding a new subsidiary or client site into the fleet is straightforward. Fork the config file, customize the per-site variables (database credentials, site-specific configuration), deploy. The new site starts at the same baseline as every other site in the fleet.
And when a critical advisory lands on a Friday afternoon for a runtime or service the fleet depends on, the response is: update the config file, run the script, review the output. Not 40 manual deployments.
The config file approach doesn't require rebuilding existing sites from scratch. The config describes the target state, so migration is a matter of writing down what each site is already running: the PHP version, the services, the relationships between them. Start with one site as the template, verify it matches production, then extend it to the rest of the fleet. The fleet starts converging from the first site that moves across.
Does every site in the fleet have to be identical?
The config file defines the shared baseline: runtime version, service versions, and infrastructure configuration. Per-site variables (database credentials, environment-specific settings, custom domains) are configured separately per project and override or extend the base config. The fleet can share a common infrastructure definition while each site remains independently configurable.
How do we manage the config file across a large fleet without a monorepo?
A common pattern is a dedicated "fleet template" repository that contains the shared config file. Individual site repositories pull from or reference that template. When the template is updated, a CI pipeline can iterate over all registered fleet projects and trigger deployments via the API. The platform API supports this pattern directly.
What happens if a deployment fails on one site mid-fleet update?
The script approach means each deployment is independent. A failure on one project doesn't block others. The script output tells you which projects succeeded and which failed, so you have a clear list to retry or investigate rather than having to work out which sites were reached and which weren't.
Can we roll back a fleet update if something goes wrong?
Yes. Because the config file is version-controlled, rolling back means reverting the commit and redeploying. Upsun also maintains deployment history per environment, so individual environments can be rolled back through the platform if needed.
How do we handle sites that legitimately need a different configuration from the rest of the fleet?
Exceptions are handled at the project level. A site that needs PHP 8.2 while the fleet standardizes on 8.3 can have that specified in its own config file, which takes precedence. The pattern is: shared template for the fleet baseline, per-project overrides for justified exceptions. The key difference from a fully manual approach is that exceptions are explicit and documented rather than accidental.