This is an example of a CDN PHP script for WordPress

This code has options for using or not using a CDN, with watermarking. It is highly specific, but shows hooks into WordPress for the way we want to use the Media Library with the AWS-SDK and the Linux2023 attached IAM Role.

Keep in mind this is not production-bullet-proof, and the issues I mentioned in the AI Article apply.

We do not provide SES examples on this site.


WordPress Media Library + Custom CDN Script


In this specific and detailed example, we only add webp/png/jpg/jpeg images to the CDN (we have a flag to switch it off) and reasonable file formats for PDF, audio/video to also be uploaded. We have added an extra media size of 1800 pixels which is also configured to be acceptable to the Avada theme. We have also added a watermark option for the largest image. We have a flag to convert images to webp format if desired. The original uploaded file is stores in ./uploads/original (until deleted). The deletions invalidate the resource in the CDN.
We do not use the CDN for .js or .css files etc. This is considered minimal in effect in terms of global geographic performance.

This script is not fully tested and revised for production but is useful to us. We have another version that allows the CDN to be shared by multiple domains, but in this case we use one domain distribution in AWS. We have another versions for Linode where there is no attached IAM Role, but rather, an .aws profile and IAM User (with policies).

This is complex. It removes the use of a CDN plugin that can slow down the website page display. Several configurations are required for it to work end-to-end. Make sure if using a watermark, that the logo and font you have are in the ./uploads directory. We do not use >2560 pixel scaled WordPress images (that adds more complications and breaks the code). We do not use uploads to day/month folders. We have not tested this, or other pligins such as sliders.

mu-plugins scripts

You need to use your own domain name, and AWS settings.

Assume /var/www/html for wordpress and nginx.

cd /var/www/html/wp-content
--> Create mu-plugins if not already
mkdir mu-plugins
chmod 2775 mu-plugins
chown nginx:nginx mu-plugins

vi web-media-policy.php

<?php
/**
 * ======================================================
 * SHA MEDIA POLICY — PRODUCTION SAFE VERSION
 * ======================================================
 * Image sizes, quality, and mime handling
 */

/* ======================================================
   SECTION A — Remove Scaled Image Threshold
====================================================== */

add_filter( 'big_image_size_threshold', '__return_false' );

/* ======================================================
   SECTION 1 — IMAGE SIZE REGISTRATION
====================================================== */

add_action('after_setup_theme', function () {

    /**
     * 1.1 — Custom square size (600x600 cropped)
     */
    add_image_size('max_1800', 1800, 1800, false);
    // a square crop would be true but WP has issues using

});


/* ======================================================
   SECTION 2 — IMAGE SIZE FILTERING - NOW IN SHA-MEDIA-PIPELINE.PHP
====================================================== */

// add_filter('intermediate_image_sizes_advanced', function ($sizes) {

    /**
     * 2.1 — Allowed size whitelist
     */

/**
    $allowed = [
        'thumbnail',
        'medium',
        'max_1800',
        'large'
    ];

    foreach ($sizes as $size => $dimensions) {
        if (!in_array($size, $allowed, true)) {
            unset($sizes[$size]);
        }
    }

    return $sizes;
});

 */


/* ======================================================
   SECTION 3 — IMAGE QUALITY SETTINGS
====================================================== */

/**
 * 3.1 — JPEG quality
 */
add_filter('jpeg_quality', function () {
    return 90;
});

/**
 * 3.2 — WebP quality
 */
add_filter('wp_editor_set_quality', function ($quality, $mime_type) {

    if ($mime_type === 'image/webp') {
        return 90;
    }

    return $quality;

}, 10, 2);


/* ======================================================
   SECTION 4 — MIME TYPE EXTENSIONS
====================================================== */

function sha_allow_media_uploads($mimes) {

    /* Images */
    $mimes['webp'] = 'image/webp';

    /* Fonts */
    $mimes['ttf']   = 'font/ttf';
    $mimes['otf']   = 'font/otf';
    $mimes['woff']  = 'font/woff';
    $mimes['woff2'] = 'font/woff2';
    $mimes['eot']   = 'application/vnd.ms-fontobject';

    /* Audio */
    $mimes['mp3']  = 'audio/mpeg';
    $mimes['wav']  = 'audio/wav';
    $mimes['ogg']  = 'audio/ogg';

    /* Video */
    $mimes['mp4']  = 'video/mp4';
    $mimes['webm'] = 'video/webm';
    $mimes['mov']  = 'video/quicktime';

    /**
     * SVG — restrict to admins only
     */
    if (current_user_can('manage_options')) {
        $mimes['svg'] = 'image/svg+xml';
    }

    return $mimes;
}

add_filter('upload_mimes', 'sha_allow_media_uploads');

[save and exit]

chown nginx:nginx web-media-policy.php;  chmod 664 web-media-policy.php

-->
Now for the main script...
Configure your watermark ['logo' => WP_CONTENT_DIR . '/uploads/logo.png',] using your own .png file. png only. For instance a white logo with some transparent background.

OPTIONS:
define('WEB_ENABLE_WEBP', true); This converts png/jpg/jpeg files to webp format (existing webp is ok). No code for other formats.
define('WEB_WEBP_ONLY', true);
define('WEB_STORE_ORIGINALS', true); Put original media Library file into ./uploads/original Please mkdir origina under uploads with 2775, and permissions.
define('WEB_ALLOWED_SIZES', ['thumbnail','medium','max_1800','large']); Here we see a restriction on the sizes we like to use, and the new 1800 pixels which is a good size for photography.
define('WEB_JPEG_QUALITY', 85); This is reasonable
define('WEB_PNG_QUALITY', 85); PNG files can suffer when converted to webp. Just check.
define('WEB_PNG_LOSSLESS', true); I prefer lossless for any png file.
define('WEB_ENABLE_WATERMARK', true); This says to put a watermark on the largest file. You can see options below for logo and text, and position, size or % size.
'font' => WP_CONTENT_DIR . '/uploads/plex-light.ttf', Make sure you have a watermark font of some sort
define('WEB_CDN_ENABLED', true); Let's use the CDN for the MEdia Library - you will see this in the URL
define('WEB_CDN_UPLOAD', true); Let's upload to CDN even if we choose not to use the CDN above
define('WEB_CDN_UPLOAD_ORIGINALS', false); I think this may be faulty - just check ./uploads/original
define('WEB_CDN_NON_IMAGES', true); This is an example of code drift after many iterations not recalling what it does. We can ask AI for an inventory. We want non-images such as PDF documents, audio etc.
'text' => '© domain.com', Use your own watermark text.
define('WEB_ALLOWED_SIZES', ['thumbnail','medium','max_1800','large']); These relate directly to the web-media-policy.php file.

We have /var/www/aws-sdk/vendor/autoload.php as our composer installation of the aws-sdk. aws-sdk is important as it is higher performance than aws cli, and does not need aws credentials on the Linux server.
The watermark has vairous options and shadow style for text.
Section 7B shows registration for our 1800 size and the Avada ability to use it.
The files have cache expire times. WE use CDN invalidation to ensure the same named image on a new upload is not using the old image.
No mention is needed of the CLoudfront Distribution ID etc. as this will be taken care of in the EC2 IAM Role. The Akamai/Linode version must use this information as it cannot attach an IAM Role.
The Role is reviously configured and attached from the EC2 console.

REPLACE:
define('WEB_BUCKET', 'cdn.domain.com'); Use your own CDN bucket. I use the same name as the CDN URL.
define('WEB_REGION', 'ap-southeast-2'); USe your own region. (This is Sydney)
define('WEB_CDN_URL', 'https://cdn.domain.com');

<--

vi web-media-pipeline.php

<?php
/**
 * ======================================================
 * WEB MEDIA PIPELINE — UNIFIED (WEB + CDN + WATERMARK FIXED)
 * ======================================================
 * FIXED:
 * - Watermark ONLY applied to final full-size image
 * - No watermark leakage into WP generated sizes
 * - Clean WebP conversion pipeline
 * - CDN + delete sync preserved
 */


/* ======================================================
   SECTION 1 — CORE FLAGS
====================================================== */

define('WEB_ENABLE_WEBP', true);
define('WEB_WEBP_ONLY', true);

define('WEB_STORE_ORIGINALS', true);

define('WEB_ALLOWED_SIZES', ['thumbnail','medium','max_1800','large']);

define('WEB_JPEG_QUALITY', 85);
define('WEB_PNG_QUALITY', 85);
define('WEB_PNG_LOSSLESS', true);


/* ======================================================
   SECTION 2 — WATERMARK CONFIG
====================================================== */

define('WEB_ENABLE_WATERMARK', true);

define('WEB_WATERMARK_OPTIONS', [

    /* LOGO */
    'logo' => WP_CONTENT_DIR . '/uploads/logo.png',

    'logo_size_mode' => 'fixed',   // fixed | percent
    'logo_fixed_px'  => 120,
    'logo_percent'   => 0.12,

    'logo_opacity' => 0.5,

    /* TEXT */
    'text' => '© domain.com',
    'font' => WP_CONTENT_DIR . '/uploads/plex-light.ttf',

    'font_size_mode' => 'fixed',   // fixed | percent
    'font_fixed_px'  => 28,
    'font_percent'   => 0.025,

    'text_color'   => '#ffffff',
    'text_opacity' => 0.6,

    /* POSITION */
    'position' => 'bottom-left', // bottom-left | bottom-right | top-left | top-right | center
    'margin'   => 60,
    'gap'      => 40,

    /* SHADOW */
    'shadow_mode'   => 'stroke_shadow', // simple | soft | stroke | stroke_shadow
    'shadow_opacity'=> 0.45,
    'shadow_offset' => 2,
]);

/* ======================================================
   SECTION 3 — CDN CONFIG
====================================================== */

define('WEB_CDN_ENABLED', true);
define('WEB_CDN_UPLOAD', true);
define('WEB_CDN_UPLOAD_ORIGINALS', false);

define('WEB_CDN_NON_IMAGES', true);

define('WEB_BUCKET', 'cdn.domain.com');
define('WEB_REGION', 'ap-southeast-2');

define('WEB_CDN_URL', 'https://cdn.domain.com');
define('WEB_SITE_URL', site_url());


/* ======================================================
   SECTION 4 — AWS SDK CLIENT (IAM ROLE EC2)
====================================================== */

function web_s3_client() {

    static $client = null;

    if ($client !== null) return $client;

    if (!class_exists(\Aws\S3\S3Client::class)) {
        require_once '/var/www/aws-sdk/vendor/autoload.php';
    }

    $client = new Aws\S3\S3Client([
        'version' => 'latest',
        'region'  => WEB_REGION,
        // IAM ROLE USED — NO credentials array required
    ]);

    return $client;
}


/* ======================================================
   SECTION 5 — MIME SAFETY
====================================================== */

add_filter('wp_check_filetype_and_ext', function($data, $file, $filename, $mimes) {

    $ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION));

    if ($ext === 'svg') {
        $data['ext']  = 'svg';
        $data['type'] = 'image/svg+xml';
        return $data;
    }

    if (in_array($ext, ['ttf','otf','woff','woff2','eot'], true)) {
        $data['ext']  = $ext;
        $data['type'] = $mimes[$ext] ?? 'application/octet-stream';
    }

    return $data;

}, 10, 4);


/* ======================================================
   SECTION 6 — WATERMARK ENGINE (UNCHANGED)
====================================================== */

function web_apply_watermark_resource($img) {

    if (!WEB_ENABLE_WATERMARK) return $img;

    $opts = WEB_WATERMARK_OPTIONS;

    imagealphablending($img, true);
    imagesavealpha($img, true);

    $img_w = imagesx($img);
    $img_h = imagesy($img);

    /* SIZE MODES */

    $logo_w = ($opts['logo_size_mode'] === 'percent')
        ? intval($img_w * $opts['logo_percent'])
        : $opts['logo_fixed_px'];

    $font_size = ($opts['font_size_mode'] === 'percent')
        ? intval($img_w * $opts['font_percent'])
        : $opts['font_fixed_px'];

    /* LOAD LOGO */

    $logo_img = null;
    $logo_h = 0;

    if (!empty($opts['logo']) && file_exists($opts['logo'])) {

        $logo_img = imagecreatefrompng($opts['logo']);

        if ($logo_img) {

            imagealphablending($logo_img, true);
            imagesavealpha($logo_img, true);

            $logo_h = intval(imagesy($logo_img) * $logo_w / imagesx($logo_img));

            $resized = imagecreatetruecolor($logo_w, $logo_h);

            imagealphablending($resized, false);
            imagesavealpha($resized, true);

            $transparent = imagecolorallocatealpha($resized, 0, 0, 0, 127);
            imagefill($resized, 0, 0, $transparent);

            imagecopyresampled(
                $resized,
                $logo_img,
                0, 0, 0, 0,
                $logo_w,
                $logo_h,
                imagesx($logo_img),
                imagesy($logo_img)
            );

            $opacity = max(0, min(1, $opts['logo_opacity']));
            $alpha = 127 * (1 - $opacity);

            imagefilter($resized, IMG_FILTER_COLORIZE, 0, 0, 0, $alpha);

            imagedestroy($logo_img);
            $logo_img = $resized;
        }
    }

    /* TEXT BOX */

    $text = $opts['text'];
    $font = $opts['font'];

    if (!file_exists($font)) return $img;

    $bbox = imagettfbbox($font_size, 0, $font, $text);

    $text_w = abs($bbox[2] - $bbox[0]);
    $text_h = abs($bbox[7] - $bbox[1]);

    $margin = $opts['margin'];
    $gap    = $opts['gap'];
    $pos    = $opts['position'];

    $logo_gap = $logo_img ? $gap : 0;

    /* LAYOUT */

    if (in_array($pos, ['bottom-left','bottom-right','top-left','top-right'])) {

        $block_w = $logo_w + $logo_gap + $text_w;
        $block_h = max($logo_h, $text_h);

        switch ($pos) {

            case 'bottom-right':
                $x = $img_w - $block_w - $margin;
                $y = $img_h - $block_h - $margin;
                break;

            case 'top-left':
                $x = $margin;
                $y = $margin;
                break;

            case 'top-right':
                $x = $img_w - $block_w - $margin;
                $y = $margin;
                break;

            default:
                $x = $margin;
                $y = $img_h - $block_h - $margin;
        }

        if ($logo_img) {
            imagecopy($img, $logo_img, $x, $y, 0, 0, $logo_w, $logo_h);
        }

        $text_x = $x + $logo_w + $logo_gap;
        $text_y = $y + ($block_h / 2) + ($text_h / 2);

    } else {

        $block_h = $logo_h + $gap + $text_h;
        $block_w = max($logo_w, $text_w);

        $x = ($img_w - $block_w) / 2;
        $y = ($img_h - $block_h) / 2;

        if ($logo_img) {
            $lx = $x + ($block_w - $logo_w) / 2;
            imagecopy($img, $logo_img, $lx, $y, 0, 0, $logo_w, $logo_h);
        }

        $text_x = $x + ($block_w - $text_w) / 2;
        $text_y = $y + $logo_h + $gap + $text_h;
    }

    /* SHADOW */

    $shadow_mode = $opts['shadow_mode'];
    $offset = $opts['shadow_offset'];

    list($sr,$sg,$sb) = [0,0,0];
    $sa = 127 * (1 - $opts['shadow_opacity']);

    $shadow = imagecolorallocatealpha($img, $sr,$sg,$sb,$sa);

    $draw = function($x,$y) use ($img,$font_size,$font,$text,$shadow,$offset,$shadow_mode){

        if ($shadow_mode === 'simple') {
            imagettftext($img,$font_size,0,$x+$offset,$y+$offset,$shadow,$font,$text);
        }
        elseif ($shadow_mode === 'soft') {
            imagettftext($img,$font_size,0,$x+1,$y+1,$shadow,$font,$text);
            imagettftext($img,$font_size,0,$x+2,$y+2,$shadow,$font,$text);
        }
        elseif ($shadow_mode === 'stroke') {
            foreach ([[-1,0],[1,0],[0,-1],[0,1]] as $o) {
                imagettftext($img,$font_size,0,$x+$o[0],$y+$o[1],$shadow,$font,$text);
            }
        }
        elseif ($shadow_mode === 'stroke_shadow') {
            foreach ([[-1,0],[1,0],[0,-1],[0,1]] as $o) {
                imagettftext($img,$font_size,0,$x+$o[0],$y+$o[1],$shadow,$font,$text);
            }
            imagettftext($img,$font_size,0,$x+$offset,$y+$offset,$shadow,$font,$text);
        }
    };

    $draw($text_x, $text_y);

    /* FINAL TEXT */

    list($r,$g,$b) = hex2rgb($opts['text_color']);
    $alpha = 127 * (1 - $opts['text_opacity']);

    $color = imagecolorallocatealpha($img, $r,$g,$b,$alpha);

    imagettftext($img, $font_size, 0, $text_x, $text_y, $color, $font, $text);

    return $img;
}

/* ======================================================
   SECTION 7 — HELPERS
====================================================== */

function hex2rgb($hex){
    $hex=str_replace("#","",$hex);

    if(strlen($hex)==3){
        return [
            hexdec(str_repeat(substr($hex,0,1),2)),
            hexdec(str_repeat(substr($hex,1,1),2)),
            hexdec(str_repeat(substr($hex,2,1),2))
        ];
    }

    return [
        hexdec(substr($hex,0,2)),
        hexdec(substr($hex,2,2)),
        hexdec(substr($hex,4,2))
    ];
}

/* ======================================================
   SECTION 7B — EXPOSE CUSTOM IMAGE SIZES TO UI
====================================================== */

/**
 * 1 — WordPress media UI (Gutenberg + classic)
 */
add_filter('image_size_names_choose', function ($sizes) {
    $sizes['max_1800'] = 'Max 1800px';
    return $sizes;
});


/**
 * 2 — Avada / Fusion Builder support -- ONLY FOR AVADA
 */
add_filter('fusion_image_sizes', function ($sizes) {
    $sizes['max_1800'] = esc_html__('Max 1800px', 'avada');
    return $sizes;
});

/* ======================================================
   SECTION 8 — WEBP PIPELINE (FIXED: NO WATERMARK HERE)
====================================================== */

add_filter('wp_handle_upload', function($upload) {

    if (!WEB_ENABLE_WEBP || empty($upload['file'])) return $upload;

    $file = $upload['file'];
    $ext  = strtolower(pathinfo($file, PATHINFO_EXTENSION));

    if (!in_array($ext, ['jpg','jpeg','png','webp'], true)) {
        return $upload;
    }

    $uploads = wp_upload_dir();

    $original_dir = trailingslashit($uploads['basedir']) . 'original';

    if (!file_exists($original_dir)) {
        wp_mkdir_p($original_dir);
    }

    $orig_name = wp_unique_filename($original_dir, basename($file));
    $original_path = $original_dir . '/' . $orig_name;

    $filename = pathinfo($file, PATHINFO_FILENAME) . '.webp';
    $unique   = wp_unique_filename($uploads['path'], $filename);
    $webp_file = $uploads['path'] . '/' . $unique;

    /* MOVE ORIGINAL */
    if (!rename($file, $original_path)) {
        copy($file, $original_path);
        @unlink($file);
    }

    /* CREATE CLEAN WEBP (NO WATERMARK) */
    switch ($ext) {
        case 'jpg':
        case 'jpeg':
            $img = imagecreatefromjpeg($original_path);
            break;
        case 'png':
            $img = imagecreatefrompng($original_path);
            imagepalettetotruecolor($img);
            imagealphablending($img, false);
            imagesavealpha($img, true);
            break;
        case 'webp':
            $img = imagecreatefromwebp($original_path);
            break;
        default:
            return $upload;
    }

    if (!$img) return $upload;

    imagewebp($img, $webp_file, WEB_JPEG_QUALITY);
    imagedestroy($img);

    $GLOBALS['web_last_original'] = $original_path;

    $upload['file'] = $webp_file;
    $upload['url']  = str_replace($uploads['basedir'], $uploads['baseurl'], $webp_file);
    $upload['type'] = 'image/webp';

    return $upload;

}, 10);


/* ======================================================
   SECTION 8B — SAVE ORIGINAL
====================================================== */

add_action('add_attachment', function($attachment_id) {

    if (!empty($GLOBALS['web_last_original'])) {
        update_post_meta($attachment_id, '_web_original_file', $GLOBALS['web_last_original']);
        unset($GLOBALS['web_last_original']);
    }

});


/* ======================================================
   SECTION 9 — IMAGE SIZES
====================================================== */

add_filter('intermediate_image_sizes_advanced', function($sizes) {
    foreach ($sizes as $size => $data) {
        if (!in_array($size, WEB_ALLOWED_SIZES, true)) {
            unset($sizes[$size]);
        }
    }
    return $sizes;
});


/* ======================================================
   SECTION 10 — WATERMARK FINAL FULL SIZE ONLY (FIXED CORE)
====================================================== */

add_filter('wp_generate_attachment_metadata', function($metadata, $attachment_id) {

    if (!WEB_ENABLE_WATERMARK) return $metadata;

    $file = get_attached_file($attachment_id);
    if (!$file || !file_exists($file)) return $metadata;

    $ext = strtolower(pathinfo($file, PATHINFO_EXTENSION));

    if ($ext !== 'webp') return $metadata;

    if (preg_match('/-\d+x\d+/', $file)) {
        return $metadata;
    }

    $img = imagecreatefromwebp($file);
    if (!$img) return $metadata;

    $img = web_apply_watermark_resource($img);

    imagewebp($img, $file, WEB_JPEG_QUALITY);
    imagedestroy($img);

    return $metadata;

}, 95, 2);


/* ======================================================
   SECTION 11 — CDN UPLOAD
====================================================== */

add_filter('wp_generate_attachment_metadata', function($metadata, $attachment_id) {

    if (!WEB_CDN_UPLOAD) return $metadata;

    $main = get_attached_file($attachment_id);
    if (!$main || !file_exists($main)) return $metadata;

    $uploads = wp_upload_dir();
    $base_dir = trailingslashit($uploads['basedir']);

    $upload = function($file) use ($base_dir) {

        if (!file_exists($file)) return;

        $type = mime_content_type($file);
        $ext  = strtolower(pathinfo($file, PATHINFO_EXTENSION));

        if (strpos($type, 'image/') === 0) {
            if (WEB_WEBP_ONLY && $ext !== 'webp') return;
            $cache = 'public, max-age=31536000, immutable';
        } else {
            if (!WEB_CDN_NON_IMAGES) return;
            $cache = 'public, max-age=86400';
        }

        $key = 'wp-content/uploads/' . ltrim(str_replace($base_dir, '', $file), '/');

        try {
            web_s3_client()->putObject([
                'Bucket'       => WEB_BUCKET,
                'Key'          => $key,
                'SourceFile'   => $file,
                'ContentType'  => $type,
                'CacheControl' => $cache,
            ]);
        } catch (\Exception $e) {
            error_log($e->getMessage());
        }
    };

    $upload($main);

    $files = glob(dirname($main) . '/' . pathinfo($main, PATHINFO_FILENAME) . '-*.*');

    foreach ($files as $file) {
        $upload($file);
    }

    return $metadata;

}, 99, 2);


/* ======================================================
   SECTION 12 — CDN URL REWRITE
====================================================== */

add_filter('wp_get_attachment_url', function($url) {

    if (!WEB_CDN_ENABLED) return $url;

    return str_replace(
        WEB_SITE_URL . '/wp-content/uploads/',
        WEB_CDN_URL . '/wp-content/uploads/',
        $url
    );
});


/* ======================================================
   SECTION 13 — DELETE SYNC
====================================================== */

add_action('delete_attachment', function($attachment_id) {

    if (!WEB_CDN_UPLOAD) return;

    $main = get_attached_file($attachment_id);
    if (!$main) return;

    $uploads  = wp_upload_dir();
    $base_dir = trailingslashit($uploads['basedir']);

    $paths = [];

    $rm = function($file) use ($base_dir, &$paths) {

        if (!$file) return;

        $relative = ltrim(str_replace($base_dir, '', $file), '/');
        $key = 'wp-content/uploads/' . $relative;

        $paths[] = '/' . $key;

        try {
            web_s3_client()->deleteObject([
                'Bucket' => WEB_BUCKET,
                'Key'    => $key,
            ]);
        } catch (\Exception $e) {
            error_log($e->getMessage());
        }

        if (file_exists($file)) {
            @unlink($file);
        }
    };

    $rm($main);

    $files = glob(dirname($main) . '/' . pathinfo($main, PATHINFO_FILENAME) . '-*.*');

    foreach ((array)$files as $file) {
        $rm($file);
    }

    $original = get_post_meta($attachment_id, '_web_original_file', true);

    if (!empty($original)) {
        $rm($original);
    }

    if (!empty($paths) && function_exists('web_cf_invalidate')) {
        web_cf_invalidate($paths);
    }

}, 10);


/* ======================================================
   END OF FILE
====================================================== */

[save and exit]

--> chmod 664 and permissions for the file.

systemctl restart php-fpm
--> or your version, such as php8.4-fpm

Other dependencies, examples shown:

-->
From www.conf (/etc/php-fpm.d)
The open_basedir shows directories we use for PHP programs. PHP has to have ability to access the directories in our scripts.
The ones we use for web-media-pipeline.php are pretty basic. I have shown a dummy use of .aws that we needed at some point, but may not be needed now. You have to test.
<--

php_admin_flag[allow_url_fopen] = on
php_admin_value[open_basedir] = /var/www:/var/www/html:/data:/tmp:/usr/share/phpMyAdmin:/data/tmp:/usr/bin:/var/lib/nginx/.aws

We have more in the path for our live system.

And in nginx.conf we had to remove a file size limit:

cd /etc/nginx

vi nginx.conf

--> Under the htto section: change client_max_body_size 10M; to your largest requried value, e.g. 50M.

AWS Configurations

We are very careful about publishing AWS examples. Read our non-liability statements.

In this example, we have cdn.domain.com as the AWS SSL certificate with the Cloudfront configuration (you would have to set this up correctly), and the same name for the local region bucket used by the CDN. Use your own values.

The S3 bucket is private, and not using KMS encryption. If you were, this code would need altering.

ACCOUNT_ID is your own AWS account ID, and DISTRIBUTION_ID is your Cloudfront distribution.

This is a policy (custom or inline) attached to the EC2 IAM Role of your EC2 Instance.

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "S3ObjectAccessScoped",
            "Effect": "Allow",
            "Action": [
                "s3:PutObject",
                "s3:GetObject",
                "s3:DeleteObject"
            ],
            "Resource": "arn:aws:s3:::cdn.domain.com/*"
        },
        {
            "Sid": "S3ListBucketScoped",
            "Effect": "Allow",
            "Action": [
                "s3:ListBucket"
            ],
            "Resource": "arn:aws:s3:::cdn.domain.com",
            "Condition": {
                "StringLike": {
                    "s3:prefix": "domain.com/*"
                }
            }
        },
        {
            "Sid": "S3BucketLocation",
            "Effect": "Allow",
            "Action": "s3:GetBucketLocation",
            "Resource": "arn:aws:s3:::cdn.domain.com"
        },
        {
            "Sid": "CloudFrontInvalidationAccess",
            "Effect": "Allow",
            "Action": "cloudfront:CreateInvalidation",
            "Resource": "arn:aws:cloudfront::ACCOUNT_ID:distribution/DISTRIBUTION_ID"
        }
    ]
}

This permits WordPress media library web-media-pipeline.php to work with the CDN.

You can check the code is validated for security with AI and also your code’s security.

This is the cdn.domain.com S3 Bucket permissions:

{
    "Version": "2012-10-17",
    "Id": "PolicyForCdn",
    "Statement": [
        {
            "Sid": "AllowCloudFrontReadOnly",
            "Effect": "Allow",
            "Principal": {
                "Service": "cloudfront.amazonaws.com"
            },
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::cdn.domain.com/*",
            "Condition": {
                "StringEquals": {
                    "AWS:SourceArn": "arn:aws:cloudfront::ACCOUNT_ID:distribution/DISTRIBUTION_ID"
                }
            }
        },
        {
            "Sid": "AllowEC2RoleObjectAccess",
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::ACCOUNT_ID:role/IAM_ROLE"
            },
            "Action": [
                "s3:PutObject",
                "s3:GetObject",
                "s3:DeleteObject"
            ],
            "Resource": "arn:aws:s3:::cdn.domain.com/*"
        },
        {
            "Sid": "AllowEC2RoleBucketAccess",
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::ACCOUNT_ID:role/IAM_ROLE"
            },
            "Action": [
                "s3:ListBucket",
                "s3:GetBucketLocation"
            ],
            "Resource": "arn:aws:s3:::cdn.domain.com"
        },
        {
            "Sid": "DenyInsecureTransport",
            "Effect": "Deny",
            "Principal": "*",
            "Action": "s3:*",
            "Resource": [
                "arn:aws:s3:::cdn.domain.com",
                "arn:aws:s3:::cdn.domain.com/*"
            ],
            "Condition": {
                "Bool": {
                    "aws:SecureTransport": "false"
                }
            }
        }
    ]
}

Where ACCOUNT_ID, DISTRIBUTION_ID as above, and IAM_ROLE is your attached IAM Role on the EC2 instance, which in turn has various policies such as above attached to the role.

If the CDN is out of sync with the local Media Library we can run a crontab script infrequently:

--> This example shows how we can exclude certain kinds of files. 
As we are actually storing PDF in the CDN, we do not use a line for exclude *.pdf, and we exclude other theme related lines for Avada.
This works as we use an IAM attached Role.

vi sync-cdn.sh

aws s3 sync /var/www/html/wp-content/uploads s3://cdn.domain.com/wp-content/uploads \
  --exclude "original/*" \
  --exclude "cdn-queue.*" \
  --exclude "*.log" \
  --exclude "fusion-scripts/*" \
  --exclude "fusion-styles/*" \
  --exclude "wp-defender/*" \
  --region ap-southeast-2 \
  --size-only \
  --only-show-errors
exit

[save and exit. chmod and permissions.]

--> Crontab for 1:45am each Sunday:  45 1 * * 0 sudo /home/ec2-user/sync_cdn.sh  > /dev/null 2>&1

Here is an example for Akamai/Linode where there is use of an .aws profile called wordpress (no attached IAM Role)
#!/bin/bash
aws s3 sync /var/www/html/wp-content/uploads s3://cdn.domain.com/wp-content/uploads \
  --exclude "original/*" \
  --exclude "cdn-queue.*" \
  --exclude "*.log" \
  --profile wordpress \
  --region ap-southeast-2 \
  --size-only \
  --only-show-errors
exit

If you wanted to exlude large PDF files to your local host, as an example, you would use:
--exclude "*.pdf" \

An example:

The Media Library uses the URL https://cdn.domain.com/wp-content/uploads/…..

If we switch the PHP flag to false, the URL is https://domain.com/wp-content/uploads/…..

If we switch back, we go back to https://cdn…. which is fine as our defaults always place the media library content into the CDN anyway. There are various scenarios we are not covering, or sync script sot ensure consistency between local and CDN.

Disclaimer: This content is provided as reference only and reflects practical experience at the time of writing. Technology and best practices change, so examples may require modification. No warranty is provided. Always test configurations on a development system before using them in production.