Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/build-ami.yml
Original file line number Diff line number Diff line change
Expand Up @@ -185,8 +185,8 @@ jobs:
echo "2. Attach an IAM instance profile with S3 read/write access to \`${{ vars.BACKUP_BUCKET }}\`"
echo "3. SSH in and restore data:"
echo " \`\`\`"
echo " sudo BACKUP_BUCKET=${{ vars.BACKUP_BUCKET }} bash /opt/mediawiki-ami/restore/restore.sh"
echo " sudo bash /opt/mediawiki-ami/restore/upgrade-1.35-to-1.43.sh"
echo " sudo BACKUP_BUCKET=${{ vars.BACKUP_BUCKET }} bash /opt/mediawiki-ami/restore.sh"
echo " sudo bash /opt/mediawiki-ami/upgrade-1.35-to-1.43.sh"
echo " \`\`\`"
echo "4. Validate, then move the Elastic IP to cut over"
} >> "$GITHUB_STEP_SUMMARY"
Expand Down
82 changes: 80 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,11 @@ You can also trigger it manually from the Actions UI with an optional dry-run.
## Step 4 — Launch the new instance

1. Launch an EC2 instance from the new AMI (t4g.medium, us-east-2, same VPC as old server)
2. Attach an IAM instance profile with `s3:GetObject` + `s3:ListBucket` on `$BACKUP_BUCKET`
2. Attach an IAM instance profile with `s3:GetObject` + `s3:ListBucket` on `$BACKUP_BUCKET`,
plus the AWS-managed `CloudWatchAgentServerPolicy` (see
[docs/s3-backup-setup.md](docs/s3-backup-setup.md#step-5--iam-policy-for-the-ec2-instance-profile)) —
the CloudWatch agent is already installed, configured, and enabled at boot by the AMI;
it just needs permission to publish.
3. Attach the same Elastic IP / security groups as the old server (but do **not** move the EIP yet)

---
Expand Down Expand Up @@ -128,6 +132,10 @@ cat /tmp/mw-update-*.txt
# Verify backup cron is installed
sudo cat /etc/cron.d/mediawiki-backup
sudo cat /etc/sysconfig/mediawiki-backup # confirm BACKUP_BUCKET is set

# Verify CloudWatch agent is running and publishing (no AccessDenied errors)
sudo systemctl status amazon-cloudwatch-agent
sudo tail -50 /var/log/amazon-cloudwatch-agent.log
```

When satisfied:
Expand Down Expand Up @@ -170,7 +178,7 @@ via `/etc/cron.d/mediawiki-backup`. It uploads to S3 with GFS rotation:

```
config/
cloudwatch/mediawiki-cwa.json ← CloudWatch agent config
cloudwatch/mediawiki-cwa.json ← CloudWatch agent config (metrics + logs, installed & enabled at boot)
cron/mediawiki-backup ← Cron job definitions
cron/mediawiki-jobs ← MediaWiki job runner cron
httpd/mediawiki.conf ← Apache vhost template
Expand Down Expand Up @@ -254,3 +262,73 @@ role that eliminates the need for long-lived access keys entirely.
| `MW_DISCOURSE_SECRET` | Discourse SSO shared secret ⚠ rotate |
| `BACKUP_BUCKET` | S3 bucket name for backups |

---

## MediaWiki extensions

`packer/scripts/05-extensions.sh` installs extensions via Composer, following
MediaWiki best practices
([Composer/For_extensions](https://www.mediawiki.org/wiki/Composer/For_extensions),
[Composer.json_best_practices](https://www.mediawiki.org/wiki/Manual:Composer.json_best_practices)).

**Key principle:** never modify MediaWiki core's `composer.json` directly.
Extensions are declared in `config/mediawiki/composer.local.json`, which is
merged automatically by the `wikimedia/composer-merge-plugin` already
configured in core's `composer.json`.

**Extension categories:**

- **Bundled in MW 1.43** (no installation needed — just `wfLoadExtension`):
AbuseFilter, CategoryTree, Cite, CiteThisPage, CodeEditor, ConfirmEdit,
DiscussionTools, Echo, Gadgets, ImageMap, InputBox, Interwiki, Linter,
LoginNotify, Math, MultimediaViewer, Nuke, OATHAuth, PageImages,
ParserFunctions, PdfHandler, Poem, README, ReplaceText, Scribunto,
SecureLinkFixer, SpamBlacklist, SyntaxHighlight_GeSHi, TemplateData,
TextExtracts, Thanks, TitleBlacklist, VisualEditor, WikiEditor
- **Installed via Composer** (`composer.local.json`): DiscourseSsoConsumer
(→ PluggableAuth), IFrameTag, TemplateStyles, JsonConfig, PluggableAuth,
WikiCategoryTagCloud

> TemplateStyles, JsonConfig, and WikiCategoryTagCloud are declared as
> `"package"` repositories (not `"vcs"`) because their upstream
> `composer.json` files lack a `"name"` field. Composer's `vcs` driver
> requires a name to resolve the package and skips branches without one
> (`"Unknown package has no name defined"`). The `"package"` type supplies
> the metadata inline, bypassing that requirement.

**How to add or update an extension:**

1. Add a VCS repository entry in `config/mediawiki/composer.local.json`.
2. Add the package `require` line with the correct version constraint.
3. For Gerrit extensions, use `dev-REL1_XX` matching the MW branch.
4. For tagged releases, use the semver tag (e.g. `5.0.2`).
5. Run `packer/scripts/05-extensions.sh` (or `composer update --no-dev` in
the MW root) to verify resolution.
6. Add the corresponding `wfLoadExtension()`/config to
`config/mediawiki/LocalSettings.php` — the **only** place extension
loading and configuration is defined. `05-extensions.sh` only installs
code into `extensions/`; it never writes to `LocalSettings.php`.

---

## Contributing

Issues and pull requests are welcome.

- **Adding/updating a MediaWiki extension** — see
[MediaWiki extensions](#mediawiki-extensions) above.
- **Changing build scripts** (`packer/scripts/*.sh`) — validate locally with
`packer-test/test-local.sh` before opening a PR; it runs the scripts in a
container against `mock-aws` without needing real AWS credentials.
- **Changing the Packer template** — run
`packer validate -var-file=my.auto.pkrvars.hcl packer/` and, where
feasible, a full `packer build` against a scratch VPC.
- **Changing backup/restore scripts** (`scripts/backup/`, `scripts/restore/`)
— test against a disposable bucket/instance; these scripts touch
production data paths.
- Keep secrets out of commits and PR descriptions; use the
`PKR_VAR_*`/GitHub Secrets mechanisms documented above.
- Open a PR against `main`; CI runs `packer-validate.yml` on every PR and
`build-ami.yml` on tag pushes.


64 changes: 55 additions & 9 deletions config/cloudwatch/mediawiki-cwa.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
{
"agent": {
"metrics_collection_interval": 60,
"logfile": "/var/log/amazon-cloudwatch-agent.log"
},
"logs": {
"logs_collected": {
"files": {
Expand All @@ -7,33 +11,75 @@
"file_path": "/var/log/httpd/mediawiki-error.log",
"log_group_name": "/mediawiki/apache/error",
"log_stream_name": "{instance_id}",
"timezone": "UTC"
"retention_in_days": 7,
"auto_removal": true
},
{
"file_path": "/var/log/httpd/mediawiki-access.log",
"log_group_name": "/mediawiki/apache/access",
"file_path": "/var/log/php_errors.log",
"log_group_name": "/mediawiki/php/error",
"log_stream_name": "{instance_id}",
"timezone": "UTC"
"retention_in_days": 7,
"auto_removal": true
},
{
"file_path": "/var/log/mariadb/slow.log",
"log_group_name": "/mediawiki/mariadb/slow",
"log_stream_name": "{instance_id}",
"timezone": "UTC"
"retention_in_days": 7,
"auto_removal": true
},
{
"file_path": "/var/log/mediawiki-backup.log",
"log_group_name": "/mediawiki/backup",
"log_stream_name": "{instance_id}",
"timezone": "UTC"
"retention_in_days": 7,
"auto_removal": true
}
]
}
}
},
"metrics": {
"namespace": "MediaWiki",
"append_dimensions": {
"InstanceId": "${aws:InstanceId}",
"InstanceType": "${aws:InstanceType}"
},
"aggregation_dimensions": [["InstanceId"]],
"metrics_collected": {
"cpu": { "measurement": ["usage_active"], "metrics_collection_interval": 60 },
"disk": { "measurement": ["used_percent"], "metrics_collection_interval": 60 },
"mem": { "measurement": ["mem_used_percent"], "metrics_collection_interval": 60 }
"cpu": {
"measurement": ["usage_active", "usage_iowait", "usage_user", "usage_system"],
"metrics_collection_interval": 60,
"totalcpu": true
},
"mem": {
"measurement": ["mem_used_percent", "mem_available_percent"],
"metrics_collection_interval": 60
},
"swap": {
"measurement": ["swap_used_percent"],
"metrics_collection_interval": 60
},
"disk": {
"measurement": ["used_percent", "inodes_free"],
"metrics_collection_interval": 60,
"resources": ["/"],
"drop_device": true
},
"diskio": {
"measurement": ["io_time", "read_bytes", "write_bytes"],
"metrics_collection_interval": 60,
"resources": ["*"]
},
"netstat": {
"measurement": ["tcp_established", "tcp_time_wait"],
"metrics_collection_interval": 60
},
"procstat": [
{ "pattern": "httpd", "measurement": ["cpu_usage", "memory_rss", "num_fds"] },
{ "pattern": "php-fpm", "measurement": ["cpu_usage", "memory_rss", "num_fds"] },
{ "pattern": "mariadbd", "measurement": ["cpu_usage", "memory_rss", "num_fds"] }
]
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion config/cron/mediawiki-backup
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@
# Edit /etc/sysconfig/mediawiki-backup to configure bucket and retention
SHELL=/bin/bash

0 8 * * * root /opt/mediawiki-ami/backup/backup-with-retention.sh >> /var/log/mediawiki-backup.log 2>&1
0 8 * * * root /opt/mediawiki-ami/backup-with-retention.sh >> /var/log/mediawiki-backup.log 2>&1

2 changes: 1 addition & 1 deletion config/httpd/mediawiki.conf
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
#
# HTTP redirects to HTTPS immediately. The HTTPS vhost uses the self-signed
# cert created by mod_ssl on first install; certbot replaces it post-launch:
# sudo bash /opt/mediawiki-ami/setup/setup-ssl.sh
# sudo bash /opt/mediawiki-ami/setup-ssl.sh

# ── HTTP: redirect to HTTPS ───────────────────────────────────────────────────
<VirtualHost *:80>
Expand Down
32 changes: 28 additions & 4 deletions config/mediawiki/LocalSettings.php
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@
$wgUploadPath = "$wgScriptPath/images";
$wgUploadDirectory = "$IP/images";
$wgMaxUploadSize = 104857600; # 100 MiB
$wgMaxImageArea = 6.4e7;

# Same additional types as 1.35 server
$wgFileExtensions = array_merge(
Expand Down Expand Up @@ -163,6 +164,7 @@
wfLoadSkin( 'Vector' );
wfLoadSkin( 'Timeless' );
wfLoadSkin( 'MonoBook' );
wfLoadSkin( 'MinervaNeue' );
$wgDefaultSkin = "timeless";

# =============================================================================
Expand Down Expand Up @@ -192,15 +194,21 @@
# Bundled extensions (shipped with MW 1.43 tarball)
# =============================================================================

wfLoadExtension( 'AbuseFilter' );
wfLoadExtension( 'CategoryTree' );
wfLoadExtension( 'Cite' );
wfLoadExtension( 'CiteThisPage' );
wfLoadExtension( 'CodeEditor' );
wfLoadExtension( 'ConfirmEdit' );
wfLoadExtension( 'DiscussionTools' );
wfLoadExtension( 'Echo' );
wfLoadExtension( 'Gadgets' );
wfLoadExtension( 'ImageMap' );
wfLoadExtension( 'InputBox' );
wfLoadExtension( 'Interwiki' );
wfLoadExtension( 'Linter' );
wfLoadExtension( 'LoginNotify' );
wfLoadExtension( 'Math' );
wfLoadExtension( 'MultimediaViewer' );
wfLoadExtension( 'Nuke' );
wfLoadExtension( 'OATHAuth' );
Expand All @@ -214,18 +222,34 @@
wfLoadExtension( 'SyntaxHighlight_GeSHi' );
wfLoadExtension( 'TemplateData' );
wfLoadExtension( 'TextExtracts' );
wfLoadExtension( 'Thanks' );
wfLoadExtension( 'TitleBlacklist' );
wfLoadExtension( 'VisualEditor' );
wfLoadExtension( 'WikiEditor' );

# =============================================================================
# Separately installed extensions (Gerrit REL1_43)
# =============================================================================
# AbuseFilter — rule-based anti-spam/vandalism filters
$wgAbuseFilterActions = [
'throttle' => true,
'warn' => true,
'disallow' => true,
'blockautopromote' => true,
'block' => true,
'tag' => true,
];

# LoginNotify — notifies users of logins from new devices/locations
$wgLoginNotifyUseEcho = true;

# Math — LaTeX formula rendering (useful for electronics/physics pages)
$wgDefaultUserOptions['math'] = 'mathml';

# Lua scripting — required by Infobox, Navbox, and excerpt templates
wfLoadExtension( 'Scribunto' );
$wgScribuntoDefaultEngine = 'luastandalone';
$wgScribuntoEngineConf['luastandalone']['luaPath'] = '/usr/bin/lua';
# NOTE: no luaPath override. Neither Scribunto's bundled binaries, LuaBinaries,
# nor LuaJIT (unsupported — phab:T184156) are available for arm64/Graviton.
# 04-mediawiki.sh compiles Lua 5.1.5 from source and installs it at
# Scribunto's own default bundled-binary path, so auto-detection just works.
$wgScribuntoUseCodeEditor = true;

# Per-template CSS — used by common Scribunto modules
Expand Down
34 changes: 28 additions & 6 deletions docs/s3-backup-setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,18 @@ aws s3api get-bucket-lifecycle-configuration --bucket "${BACKUP_BUCKET}"

## Step 5 — IAM policy for the EC2 instance profile

The instance needs permission to read and write within the backup prefix.
Attach the following inline policy to the instance's IAM role:
The instance needs permission to read and write within the backup prefix, and
to publish metrics/logs to CloudWatch (the agent is installed and enabled by
`06-finalize.sh`, but will silently fail to publish without this permission).

Attach the AWS-managed **`CloudWatchAgentServerPolicy`** plus the following
inline policy to the instance's IAM role:

```bash
aws iam attach-role-policy \
--role-name <instance-role-name> \
--policy-arn arn:aws:iam::aws:policy/CloudWatchAgentServerPolicy
```

```json
{
Expand All @@ -114,6 +124,18 @@ Attach the following inline policy to the instance's IAM role:
}
```

### Verify the agent is running and publishing

```bash
sudo systemctl status amazon-cloudwatch-agent
sudo /opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-ctl -a status
sudo tail -50 /var/log/amazon-cloudwatch-agent.log
```

If the role is missing `CloudWatchAgentServerPolicy`, the log shows
`AccessDenied`/`UnauthorizedOperation` errors even though the service is
`active (running)`.

---

## Step 6 — Configure the instance
Expand Down Expand Up @@ -153,7 +175,7 @@ aws s3 cp "s3://${BACKUP_BUCKET}/backups/daily/manifest.txt" - --region "${AWS_R

```bash
sudo BACKUP_BUCKET=my-mediawiki-backups \
bash /opt/mediawiki-ami/backup/backup-with-retention.sh
bash /opt/mediawiki-ami/backup-with-retention.sh
```

### Verify lifecycle rules are expiring objects
Expand All @@ -177,17 +199,17 @@ For the initial migration from 1.35, also run `scripts/restore/upgrade-1.35-to-1
```bash
# Restore from latest daily backup
sudo BACKUP_BUCKET=my-mediawiki-backups \
bash /opt/mediawiki-ami/restore/restore.sh
bash /opt/mediawiki-ami/restore.sh

# Restore from a specific weekly backup
sudo BACKUP_BUCKET=my-mediawiki-backups \
BACKUP_TIMESTAMP=weekly/2026-W24 \
bash /opt/mediawiki-ami/restore/restore.sh
bash /opt/mediawiki-ami/restore.sh

# Restore from a specific monthly backup
sudo BACKUP_BUCKET=my-mediawiki-backups \
BACKUP_TIMESTAMP=monthly/2026-05 \
bash /opt/mediawiki-ami/restore/restore.sh
bash /opt/mediawiki-ami/restore.sh
```

---
Expand Down
Loading
Loading