Peter Davies

Ramblings

PHP 5.6 on Elastic Beanstalk, and mounting storage

A legacy PHP application needed to move onto Elastic Beanstalk, and it would not run on anything newer than PHP 5.6. Beanstalk's PHP 5.6 platform had been retired; the current ones run PHP 7.x and 8.x on Amazon Linux 2. Amazon Linux 2 is close enough to CentOS 7 that Remi Collet's repository works on it, and Remi still builds PHP 5.6 as a separate software collection, so the plan was: take the stock Amazon Linux 2 PHP platform with nginx, swap its PHP for 5.6, and keep nginx and Beanstalk's own scripts happy.

PHP 5.6 has had no security fixes since the end of 2018. This is a way to buy time while the code is brought up to date, not a place to stay.

Swapping PHP by hand

I did this on a running instance over SSH first to get it right.

Stop the web stack and remove the platform's libzip. Remi's zip extension needs its own build of the library, and removing the old one also removes the platform PHP packages that depend on it:

sudo service nginx stop
sudo service php-fpm stop
sudo yum remove -y libzip

Add EPEL and Remi's EL7 repository, enable the PHP 5.6 collection, and install the libraries the zip extension depends on:

sudo yum install -y https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm
sudo yum install -y https://rpms.remirepo.net/enterprise/remi-release-7.rpm
sudo yum-config-manager --enable remi-php56
sudo yum install -y libzstd
sudo yum --enablerepo=remi install -y libzip-last

Then PHP 5.6 and the extensions the application uses:

sudo yum install -y php56 php56-php-cli php56-php-common php56-php-fpm \
  php56-php-mysql php56-php-gd php56-php-mbstring php56-php-xml \
  php56-php-intl php56-php-opcache php56-php-process php56-php-pear \
  php56-php-pecl-jsonc php56-php-pecl-zip

Everything installs under /opt/remi/php56/root, alongside the system rather than over it. The php56 command runs the 5.6 CLI, and scl enable php56 bash gives you a shell where plain php is 5.6.

Making Beanstalk's config fit

When yum removed the platform PHP packages it kept their edited config files as .rpmsave. Those files are Beanstalk's: the nginx upstream that points at the PHP-FPM socket, and the FPM pool that listens on it. Put them back, and give the 5.6 FPM a copy of the pool:

sudo mv /etc/nginx/conf.d/php-fpm.conf.rpmsave /etc/nginx/conf.d/php-fpm.conf
sudo mv /etc/php-fpm.d/www.conf.rpmsave /etc/php-fpm.d/www.conf
sudo cp /etc/php-fpm.d/www.conf /opt/remi/php56/root/etc/php-fpm.d/
sudo mkdir -p /run/php-fpm

Beanstalk's deployment scripts restart a service called php-fpm. The 5.6 service is called php56-php-fpm, so give systemd a php-fpm unit that runs it. A copy in /etc/systemd/system takes priority over anything under /usr/lib and survives package updates:

sudo cp /usr/lib/systemd/system/php56-php-fpm.service /etc/systemd/system/php-fpm.service
sudo systemctl daemon-reload

Start it all and check:

echo "<?php phpinfo();" | sudo tee /var/www/html/index.php
sudo service php-fpm start
sudo service nginx start

The page should report PHP 5.6 and the FPM/FastCGI server API. Delete index.php afterwards.

Making it survive

Beanstalk replaces instances whenever it scales or an instance fails a health check, and anything done by hand over SSH goes with them. On Amazon Linux 2 platforms the place for this is a platform hook: an executable script in your application bundle at .platform/hooks/prebuild/, which runs as root on every deployment before the application is put in place. Put the commands above into, say, .platform/hooks/prebuild/01-php56.sh with a guard at the top so it does nothing on an instance that already has 5.6:

#!/bin/bash
set -euo pipefail
if rpm -q php56-php-fpm > /dev/null 2>&1; then
  exit 0
fi
# the install steps from above, without sudo

Make it executable before you zip the bundle (chmod +x), or Beanstalk ignores it. Copy it to .platform/confighooks/prebuild/ as well if configuration changes should run it too.

Mounting an S3 bucket with s3fs

The application also expected a directory of data files on local disk, and the files lived in an S3 bucket. s3fs mounts a bucket as a FUSE filesystem. It's in EPEL for Amazon Linux 2 (yum install s3fs-fuse) and in the main archive on Ubuntu (apt-get install s3fs).

The quick way you'll see everywhere is a credentials file holding an access key:

echo "<access-key-id>:<secret-access-key>" | sudo tee /etc/passwd-s3fs
sudo chmod 600 /etc/passwd-s3fs

Don't. A long-lived key sitting in a file on every instance ends up in images, backups and support tickets, and it has to be rotated by hand. On EC2, including under Beanstalk, give the instance profile access to the bucket and let s3fs fetch temporary credentials from the instance metadata:

sudo mkdir -p /mnt/data
sudo s3fs example-bucket /mnt/data -o iam_role=auto -o allow_other

The role needs no more than this on the one bucket:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "s3:ListBucket",
      "Resource": "arn:aws:s3:::example-bucket"
    },
    {
      "Effect": "Allow",
      "Action": ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"],
      "Resource": "arn:aws:s3:::example-bucket/*"
    }
  ]
}

For a mount at boot, in /etc/fstab:

example-bucket  /mnt/data  fuse.s3fs  _netdev,allow_other,iam_role=auto  0  0

allow_other lets users other than the one who mounted it, such as the web server user, read the files. _netdev waits for the network.

Bear in mind S3 is not a disk. There's no file locking, a rename is a copy and a delete, and listing a big directory is slow. It's fine for files that are written once and read many times. If the application needs real shared storage that several instances write to, EFS is the better fit: install amazon-efs-utils and mount it with fs-0123456789abcdef0:/ /mnt/efs efs _netdev,tls 0 0 in fstab, with the instances' security group allowed to reach the file system on NFS (TCP 2049).

Today

Amazon Linux 2 reached end of support in 2026, and CentOS 7, whose packages this relies on, ended in 2024. Any application still on PHP 5.6 needs to move.