Hosting Wordpress on Serverless Architecture

0

Hi, I am preparing serverless architecture for wordpress hosting on AWS Lambda. I have referred few architecture based on serverless on (https://keita.blog/2019/06/27/serverless-wordpress-on-aws-lambda/) and here they used cloudfront, apigateway and lambda functions. I have prepared the PHP7.4 runtime for wordpress with required dependencies by referring (https://github.com/stackery/php-lambda-layer.git). Dependencies are installed by using build.sh script file as shown below

#!/bin/bash -e

amazon-linux-extras enable php7.4
yum clean metadata
yum install php-mbstring  php-pgsql php-cli php-pdo php-fpm php-json pdo_mysql php-mysqlnd  php-mysqli -y
yum install libncurses.so.5 -y
mkdir /tmp/layer
cd /tmp/layer
cp /opt/layer/bootstrap .
cp /opt/layer/php.ini .

mkdir bin
cp /usr/bin/php bin/


mkdir lib
for lib in libncurses.so.6 libtinfo.so.6 libpcre.so.1; do
  cp "/usr/lib64/${lib}" lib/
done

cp /usr/lib64/libedit.so.0 lib/
cp /usr/lib64/libpq.so.5 lib/
cp -a /usr/lib64/php lib/

zip -r /opt/layer/php74.zip .

bootstrap file

#!/opt/bin/php -c/opt/php.ini
<?php

error_reporting(E_ALL | E_STRICT);

$AWS_LAMBDA_RUNTIME_API = getenv('AWS_LAMBDA_RUNTIME_API');

/* https://gist.github.com/henriquemoody/6580488 */
$http_codes = [100=>'Continue',101=>'Switching Protocols',102=>'Processing',200=>'OK',201=>'Created',202=>'Accepted',203=>'Non-Authoritative Information',204=>'No Content',205=>'Reset Content',206=>'Partial Content',207=>'Multi-Status',208=>'Already Reported',226=>'IM Used',300=>'Multiple Choices',301=>'Moved Permanently',302=>'Found',303=>'See Other',304=>'Not Modified',305=>'Use Proxy',306=>'Switch Proxy',307=>'Temporary Redirect',308=>'Permanent Redirect',400=>'Bad Request',401=>'Unauthorized',402=>'Payment Required',403=>'Forbidden',404=>'Not Found',405=>'Method Not Allowed',406=>'Not Acceptable',407=>'Proxy Authentication Required',408=>'Request Timeout',409=>'Conflict',410=>'Gone',411=>'Length Required',412=>'Precondition Failed',413=>'Request Entity Too Large',414=>'Request-URI Too Long',415=>'Unsupported Media Type',416=>'Requested Range Not Satisfiable',417=>'Expectation Failed',418=>'I\'m a teapot',419=>'Authentication Timeout',420=>'Enhance Your Calm',420=>'Method Failure',422=>'Unprocessable Entity',423=>'Locked',424=>'Failed Dependency',424=>'Method Failure',425=>'Unordered Collection',426=>'Upgrade Required',428=>'Precondition Required',429=>'Too Many Requests',431=>'Request Header Fields Too Large',444=>'No Response',449=>'Retry With',450=>'Blocked by Windows Parental Controls',451=>'Redirect',451=>'Unavailable For Legal Reasons',494=>'Request Header Too Large',495=>'Cert Error',496=>'No Cert',497=>'HTTP to HTTPS',499=>'Client Closed Request',500=>'Internal Server Error',501=>'Not Implemented',502=>'Bad Gateway',503=>'Service Unavailable',504=>'Gateway Timeout',505=>'HTTP Version Not Supported',506=>'Variant Also Negotiates',507=>'Insufficient Storage',508=>'Loop Detected',509=>'Bandwidth Limit Exceeded',510=>'Not Extended',511=>'Network Authentication Required',598=>'Network read timeout error',599=>'Network connect timeout error'];

function start_webserver() {
  $SERVER_STARTUP_TIMEOUT = 1000000; // 1 second

  $pid = pcntl_fork();
  switch($pid) {
    case -1:
      die("Failed to fork webserver process\n");

    case 0:
      // exec the command
      $HANDLER = getenv('_HANDLER');
      $phpMinorVersion = PHP_MINOR_VERSION;
      $handler_components = explode('/', $HANDLER);
      $handler_filename = array_pop($handler_components);
      $handler_path = implode('/', array_merge(['/var/task'], $handler_components));
      chdir($handler_path);
      exec("PHP_INI_SCAN_DIR=/opt/ php -S localhost:8000 -c /opt/php.ini -d extension_dir=/opt/lib/php/modules '$handler_filename'");
      exit;

    default:
      // Wait for child server to start
      $start = microtime(true);

      do {
        if (microtime(true) - $start > $SERVER_STARTUP_TIMEOUT) {
          die("Webserver failed to start within one second\n");
        }

        usleep(1000);
        $fp = @fsockopen('localhost', 8000, $errno, $errstr, 1);
      } while ($fp == false);

      fclose($fp);
  }
}

function fail($AWS_LAMBDA_RUNTIME_API, $invocation_id, $message) {
  $ch = curl_init("http://$AWS_LAMBDA_RUNTIME_API/2018-06-01/runtime/invocation/$invocation_id/response");

  $response = array();

  $response['statusCode'] = 500;
  $response['body'] = $message;

  $response_json = json_encode($response);

  curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
  curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
  curl_setopt($ch, CURLOPT_POSTFIELDS, $response_json);
  curl_setopt($ch, CURLOPT_HTTPHEADER, array(
    'Content-Type: application/json',
    'Content-Length: ' . strlen($response_json)
  ));

  curl_exec($ch);
  curl_close($ch);
}

start_webserver();

while (true) {
  $ch = curl_init("http://$AWS_LAMBDA_RUNTIME_API/2018-06-01/runtime/invocation/next");

  curl_setopt($ch, CURLOPT_FOLLOWLOCATION, TRUE);
  curl_setopt($ch, CURLOPT_FAILONERROR, TRUE);

  $invocation_id = '';

  curl_setopt($ch, CURLOPT_HEADERFUNCTION, function ($ch, $header) use (&$invocation_id) {
    if (!preg_match('/:\s*/', $header)) {
      return strlen($header);
    }

    [$name, $value] = preg_split('/:\s*/', $header, 2);

    if (strtolower($name) == 'lambda-runtime-aws-request-id') {
      $invocation_id = trim($value);
    }

    return strlen($header);
  });

  $body = '';

  curl_setopt($ch, CURLOPT_WRITEFUNCTION, function ($ch, $chunk) use (&$body) {
    $body .= $chunk;

    return strlen($chunk);
  });

  curl_exec($ch);

  if (curl_error($ch)) {
    die('Failed to fetch next Lambda invocation: ' . curl_error($ch) . "\n");
  }

  if ($invocation_id == '') {
    die("Failed to determine Lambda invocation ID\n");
  }

  curl_close($ch);

  if (!$body) {
    die("Empty Lambda invocation response\n");
  }

  $event = json_decode($body, TRUE);

  if (!array_key_exists('requestContext', $event)) {
    fail($AWS_LAMBDA_RUNTIME_API, $invocation_id, 'Event is not an API Gateway request');
    continue;
  }

  $uri = $event['path'];

  if (array_key_exists('multiValueQueryStringParameters', $event) && $event['multiValueQueryStringParameters']) {
    $first = TRUE;
    foreach ($event['multiValueQueryStringParameters'] as $name => $values) {
      foreach ($values as $value) {
        if ($first) {
          $uri .= "?";
          $first = FALSE;
        } else {
          $uri .= "&";
        }

        $uri .= $name;

        if ($value != '') {
          $uri .= '=' . $value;
        }
      }
    }
  }

  $ch = curl_init("http://localhost:8000$uri");

  curl_setopt($ch, CURLOPT_FOLLOWLOCATION, FALSE);

  if (array_key_exists('multiValueHeaders', $event)) {
    $headers = array();

    foreach ($event['multiValueHeaders'] as $name => $values) {
      foreach ($values as $value) {
        array_push($headers, "${name}: ${value}");
      }
    }

    curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
  }

  curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $event['httpMethod']);

  if (array_key_exists('body', $event)) {
    $body = $event['body'];
    if (array_key_exists('isBase64Encoded', $event) && $event['isBase64Encoded']) {
      $body = base64_decode($body);
    }
  } else {
    $body = '';
  }

  if (strlen($body) > 0) {
    curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
  }

  $response = array();
  $response['multiValueHeaders'] = array();
  $response['body'] = '';

  curl_setopt($ch, CURLOPT_HEADERFUNCTION, function ($ch, $header) use (&$response) {
    if (preg_match('/HTTP\/1.1 (\d+) .*/', $header, $matches)) {
      $response['statusCode'] = intval($matches[1]);
      return strlen($header);
    }

    if (!preg_match('/:\s*/', $header)) {
      return strlen($header);
    }

    [$name, $value] = preg_split('/:\s*/', $header, 2);

    $name = trim($name);
    $value = trim($value);

    if ($name == '') {
      return strlen($header);
    }

    if (!array_key_exists($name, $response['multiValueHeaders'])) {
      $response['multiValueHeaders'][$name] = array();
    }

    array_push($response['multiValueHeaders'][$name], $value);

    return strlen($header);
  });

  curl_setopt($ch, CURLOPT_WRITEFUNCTION, function ($ch, $chunk) use (&$response) {
    $response['body'] .= $chunk;

    return strlen($chunk);
  });

  curl_exec($ch);
  curl_close($ch);

  $ch = curl_init("http://$AWS_LAMBDA_RUNTIME_API/2018-06-01/runtime/invocation/$invocation_id/response");

  $isALB = array_key_exists("elb", $event['requestContext']);
  if ($isALB) { // Add Headers For ALB
    $status = $response["statusCode"];
    if (array_key_exists($status, $http_codes)) {
        $response["statusDescription"] = "$status ". $http_codes[$status];
    } else {
        $response["statusDescription"] = "$status Unknown";
    }
    $response["isBase64Encoded"] = false;
  }
  $response_json = json_encode($response);
  curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
  curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
  curl_setopt($ch, CURLOPT_POSTFIELDS, $response_json);
  if (!$isALB){
    curl_setopt($ch, CURLOPT_HTTPHEADER, array(
      'Content-Type: application/json',
      'Content-Length: ' . strlen($response_json)
    ));
  }
  curl_exec($ch);
  curl_close($ch);
}

?>

Refer SAM Template here https://github.com/keichan34/wordpress-on-lambda/blob/master/template.yaml

In source code, handler.php is

<?php

// A special handler.

// All requests through API Gateway are HTTPS.
$_SERVER['HTTPS'] = 'on';

$extension_map = array(
    "css" => "text/css",
    "js" => "application/javascript",
    "png" => "image/png",
    "jpeg" => "image/jpeg",
    "jpg" => "image/jpeg",
    "svg" => "image/svg+xml"
);

$request_uri = explode("?", $_SERVER['REQUEST_URI']);
$local_file_path = $_SERVER['DOCUMENT_ROOT'] . $request_uri[0];

if ( $local_file_path == __FILE__ ) {
    http_response_code(400);
    echo 'Sorry';
    exit();
}

// echo '<pre>';
// var_dump($_SERVER);
// echo '</pre>';

// echo "X-ScriptFilename: " . $local_file_path;
// exit();

$split = explode(".", $local_file_path);
$extension = end($split);
$mapped_type = $extension_map[$extension];

if ( $mapped_type && file_exists( $local_file_path ) ) {
    header("Content-Type: {$mapped_type}");
    readfile($local_file_path);

} elseif ( $extension == "php" && file_exists( $local_file_path ) ) {
    header("X-ExecFile: {$local_file_path}");
    require( $local_file_path );

} elseif ( substr($local_file_path, -1) == "/" && file_exists( $local_file_path . "index.php" ) ) {
    $exec_file_path = $local_file_path . "index.php";
    header("X-ExecFile: {$exec_file_path}");
    require( $exec_file_path );

} else {
    $exec_file_path = dirname(__FILE__) . '/index.php';
    header("X-ExecFile: {$exec_file_path}");
    require( $exec_file_path );
}

So while deploying this architecture I am facing issue in routing. While making request from lambda Url it shows body with empty response and returns 302 status code like below "multiValueHeaders":{"Host":["localhost:8000"],"Date":["Wed, 27 Sep 2023 11:55:48 GMT"],"Connection":["close"],"X-Powered-By":["PHP\/7.4.33"],"X-ExecFile":["\/var\/task\/index.php"],"Expires":["Wed, 11 Jan 1984 05:00:00 GMT"],"Cache-Control":["no-cache, must-revalidate, max-age=0"],"X-Redirect-By":["WordPress"],"Location":["https:\/\/localhost:8000\/wp-admin\/install.php"],"Content-type":["text\/html; charset=UTF-8"]},"body":"","statusCode":302} and also when making request via cloudfront domain it is redirecting to apiurl with wp-admin/install.php path but return as forbidden message like below Forbidden Error

And if we make cloudfront url (eg: <cloudfrontdns>/wp-admin/install.php it shows the wordpress install page it show like below image wordpress Install page and able configure site settings and even it showed the basic blog page with cloudfront dns. But not able to login to admin page as it shows same Forbidden error. So some pages like database error connection page, basic sample blog page are able to access but main issue is not able to login to wp-admin page as it shows only forbidden error. So Please help me to resolve the issue,and help to solve why it is not able to move to admin page. Note: Not able to redirect to wp-admin page as it shows Forbidden.

No Answers

You are not logged in. Log in to post an answer.

A good answer clearly answers the question and provides constructive feedback and encourages professional growth in the question asker.

Guidelines for Answering Questions