Обмен данными между несколькими серверами через AWS S3

При предоставлении некоторой функциональности для обработки файла, загруженного пользователем, файл должен быть доступен для процесса на протяжении всего выполнения. Простая операция загрузки и сохранения не вызывает проблем. Однако, если в дополнение файл должен быть обработан перед сохранением, и приложение работает на нескольких серверах за балансера нагрузки, то мы должны убедиться, что файл доступен для того, какой сервер работает процесс в каждый раз.

Например, многоступенчатая функция «Загрузить пользовательский аватар» может потребовать от пользователя загрузить аватар на шаг 1, обрезать его на шаг 2 и, наконец, сохранить на шаг3. После того, как файл загружен на сервер на шаг 1, файл должен быть доступен для того, какой сервер обрабатывает запрос на шаги 2 и 3, которые могут быть или не быть одинаковыми для шага 1.

Наивным подходом было бы копирование загруженного файла на шаг 1 на все другие серверы, так что файл будет доступен на всех из них. Однако такой подход не только чрезвычайно сложен, но и неосуществим: например, если сайт работает на сотнях серверов, из нескольких регионов, то это невозможно сделать.

Возможным решением является включение «липких сеансов» на балансере нагрузки, который всегда будет назначать один и тот же сервер для данного сеанса. Затем шаги 1, 2 и 3 будут обработаны на том же сервере, и файл, загруженный на этот сервер на шаг 1, будет по-прежнему находиться там для шагов 2 и 3. Тем не менее, липкие сеансы не являются полностью надежными: если между шагами 1 и 2, что сервер разбился, то балансилер нагрузки придется назначить другой сервер, нарушая функциональность и пользовательский опыт. Кроме того, всегдае назначение одного и того же сервера для сеанса может, при особых обстоятельствах, привести к замедлению времени отклика с перегруженного сервера.

Более правильным решением является сохранение копии файла в репозитории, доступного для всех серверов. Затем, после того, как файл будет загружен на сервер на шаг 1, этот сервер загрузит его в репозиторий (или, в качестве альтернативы, файл может быть загружен в репозиторий непосредственно от клиента, минуя сервер); шаг обработки сервера 2 загрузит файл из репозитория, манипулирует им и загружает его там снова; и, наконец, шаг обработки сервера 3 загрузит его из репозитория и сохранит его.

В этой статье я опишу это последнее решение, основанное на приложении WordPress, храняющем файлы на Amazon Web Services (AWS) Simple Storage Service (S3) (решение для хранения и извлечения данных облачных объектов), работающее через AWS SDK.

Примечание 1: Для простой функциональности, такой как обрезка аватаров, другим решением было бы полностью обойти сервер и реализовать его непосредственно в облаке через функции Lambda. Но поскольку эта статья о подключении приложения, работающего на сервере, с AWS S3, мы не рассматриваем это решение.

Примечание 2: Для использования AWS S3 (или любой другой службы AWS) нам необходимо иметь учетную запись пользователя. Amazon предлагает бесплатный уровень здесь в течение 1 года, что достаточно хорошо для экспериментов с их услугами.

Примечание 3: Есть третья сторона плагинов для загрузки файлов из WordPress на S3. Одним из таких плагинов является WP Media Offload (облегченная версия доступна здесь),которая обеспечивает отличную функцию: он бесшовно передает файлы, загруженные в Медиа-библиотеку в ведро S3, что позволяет отделить содержимое сайта (например, все под /wp-контентом/загрузками) из кода приложения. Разъединяя содержимое и код, мы можем развернуть наше приложение WordPress с помощью Git (в противном случае мы не можем, так как загруженный пользователем контент не размещается в репозитории Git) и размещать приложение на нескольких серверах (в противном случае, каждый сервер должен будет держать копия всего пользовательского контента.)

Создание ковша

При создании ведра, мы должны обратить внимание на название ведра: Каждое название ведра должно быть глобально уникальным в сети AWS, так что даже если мы хотели бы назвать наше ведро что-то простое, как «аватары», что имя может быть принято, то мы можем выбрать что-то более отличительное, как «аватары-имя-оф-моя компания».

Нам также нужно будет выбрать регион, в котором находится ведро (регион — это физическое местоположение, где расположен центр обработки данных, с местами по всему миру).

Регион должен быть таким же, как и там, где развертывается наше приложение, так что доступ к S3 во время выполнения процесса будет быстрым. В противном случае пользователю может потребоваться дополнительная секунда от загрузки/загрузки изображения в/из удаленного места.

Примечание: Имеет смысл использовать S3 в качестве решения для хранения облачных объектов только в том случае, если мы также используем сервис Amazon для виртуальных серверов в облаке EC2для работы приложения. Если вместо этого мы полагаемся на некоторые другие компании для размещения приложения, такие как Microsoft Azure или DigitalOcean, то мы также должны использовать их облачные услуги хранения объектов. В противном случае, наш сайт будет страдать накладные расходы от данных, путешествующих между сетями различных компаний.

На скриншотах ниже мы увидим, как создать ведро, где загрузить пользовательские аватары для обрезки. Сначала мы направимся на приборную панель S3 и нажмем на кнопку «Создать ведро»:

S3 dashboard
Панель мониторинга S3, показывающая все наши существующие ведра. (Большой предварительный просмотр)

Затем мы ввешаем в ведро имя (в данном случае, «аватары-разгром») и выбрать регион («ЕС (Франкфурт)»):

Create a bucket screen
Создание ведра через в S3. (Большой предварительный просмотр)

Только название ведра и область являются обязательными. Для следующих шагов мы можем сохранить параметры по умолчанию, поэтому мы нажимаем на «Следующий», пока, наконец, нажав на «Создать ведро», и с этим, мы будем иметь ведро создан.

Настройка пользовательских разрешений

При подключении к AWS через SDK мы должны будем ввести учетные данные пользователей (пара идентификатора ключа доступа и секретного ключа доступа), чтобы подтвердить, что у нас есть доступ к запрашиваемым службам и объектам. Разрешения пользователей могут быть очень общими (роль «админ» может делать все) или очень гранулированным, просто предоставление разрешения на конкретные операции, необходимые и ничего больше.

Как правило, чем конкретнее наши разрешения, тем лучше, чтобы избежать проблем безопасности. При создании нового пользователя нам необходимо создать политику, которая представляет собой простой документ JSON, в котором перечислены разрешения, которые должны быть предоставлены пользователю. В нашем случае, наши пользовательские разрешения будут предоставлять доступ к S3, для ведра «аватары-разгром», для операций «Put» (для загрузки объекта), «Получить» (для загрузки объекта), и «Список» (для перечисления всех объектов в ведро), в результате чего следующие Политики:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "s3:Put*",
                "s3:Get*",
                "s3:List*"
            ],
            "Resource": [
                "arn:aws:s3:::avatars-smashing",
                "arn:aws:s3:::avatars-smashing/*"
            ]
        }
    ]
}

На скриншотах ниже мы можем увидеть, как добавить пользовательские разрешения. Мы должны перейти к панели мониторинга identity and Access Management (IAM):

IAM dashboard
Панель мониторинга IAM, в которой перечислены все созданные нами пользователи. (Большой предварительный просмотр)

В приборной панели мы нажимаем на «Пользователей» и сразу после этого на «Добавить пользователя». На странице Add User мы выбираем имя пользователя («crop-avatars») и отмечаем «Программный доступ» в качестве типа Access, который обеспечит идентификатор ключа доступа и секретный ключ доступа для подключения через SDK:

Add user page
Добавление нового пользователя. (Большой предварительный просмотр)

Затем мы нажимаем на кнопку «Следующий: Разрешения», нажимаем на кнопку «Прикрепите существующие политики напрямую» и нажмите на кнопку «Создать политику». Это откроет новую вкладку в браузере со страницей политики Create. Мы нажимаем на вкладку JSON и введомим код JSON для политики, определяемой выше:

Create policy page
Создание политики, предоставляющей операции «Получить», «Почта» и «Список» на ведре «аватары-разгромы». (Большой предварительный просмотр)

Затем мы нажимаем на политику обзора, даем ей имя («CropAvatars»), и, наконец, нажмите на политику создания. Создавая политику, мы возвращаемся к предыдущей вкладке, выбираем политику CropAvatars (возможно, потребуется обновить список политик, чтобы увидеть ее), нажмите на Следующий: Обзор и, наконец, на создание пользователя. После этого мы можем, наконец, скачать идентификатор ключа доступа и секретный ключ доступа (пожалуйста, обратите внимание, что эти учетные данные доступны для этого уникального момента; если мы не скопировать или скачать их сейчас, мы должны создать новую пару):

User creation success page
После создания пользователя нам предлагается уникальное время для загрузки учетных данных. (Большой предварительный просмотр)

Подключение к AWS через SDK

SDK доступен через множество языков. Для wordPress приложение, мы требуем SDK для PHP, которые могут быть загружены здесь,и инструкции о том, как установить его здесь.

После создания ведра, готовых учетных данных пользователей и установки SDK, мы можем начать загрузку файлов на S3.

Загрузка и загрузка файлов

Для удобства мы определяем учетные данные пользователей и область как константы в файле wp-config.php:

define ('AWS_ACCESS_KEY_ID', '...'); // Your access keydefine ('AWS_SECRET_ACCESS_KEY', '...'); // Your secret access key
define ('AWS_REGION', 'eu-central-1'); // Region where the bucket is located. This is the regionfor "EU (Frankfurt)"

В нашем случае мы реализуем функциональность аватара культур, для которой аватары будут храниться на «аватаре-разгромном» ведре. Тем не менее, в нашем приложении у нас может быть несколько других ведер для других функций, требующих выполнения тех же операций загрузки, загрузки и листинга файлов. Таким образом, мы реализуем общие методы на абстрактном AWS_S3 классе, и мы получаем входы, такие как имя ведра, определенное через функцию, get_bucket в реализации классов ребенка.

// Load the SDK and import the AWS objects
require 'vendor/autoload.php';
use AwsS3S3Client;
use AwsExceptionAwsException;

// Definition of an abstract class
abstract class AWS_S3 {

  protected function get_bucket() {

    // The bucket name will be implemented by the child class
    return '';
  }
}

S3ClientКласс предоставляет API для взаимодействия с S3. Мы мгновения его только тогда, когда это необходимо (через ленивый инициализации), и сохранить ссылку на него в соответствии $this->s3Client с держать с использованием того же экземпляра:

abstract class AWS_S3 {

  // Continued from above...

  protected $s3Client;

  protected function get_s3_client() {

    // Lazy initialization
    if (!$this->s3Client) {

      // Create an S3Client. Provide the credentials and region as defined through constants in wp-config.php
      $this->s3Client = new S3Client([
        'version' => '2006-03-01',
        'region' => AWS_REGION,
        'credentials' => [
          'key' => AWS_ACCESS_KEY_ID,
          'secret' => AWS_SECRET_ACCESS_KEY,
        ],
      ]);
    }

    return $this->s3Client;
  }
}

Когда мы имеем дело с $file в нашем приложении, эта переменная содержит абсолютный путь к файлу на диске (например), /var/app/current/wp-content/uploads/users/654/leo.jpg но при загрузке файла в S3 мы не должны хранить объект под тем же путем. В частности, мы должны удалить первоначальный бит, касающийся системной информации /var/app/current () по соображениям безопасности, и по желанию мы можем удалить /wp-content бит (так как все файлы хранятся в этой папке, это избыток информации), сохраняя только относительный путь к файл ( /uploads/users/654/leo.jpg ). Удобно, что это может быть достигнуто путем удаления все после WP_CONTENT_DIR от абсолютного пути. Функции get_file и get_file_relative_path ниже переключаются между абсолютными и относительными файловыми путями:

abstract class AWS_S3 {

  // Continued from above...

  function get_file_relative_path($file) {

    return substr($file, strlen(WP_CONTENT_DIR));
  }

  function get_file($file_relative_path) {

    return WP_CONTENT_DIR.$file_relative_path;
  }
}

При загрузке объекта на S3 мы можем установить, кому предоставляется доступ к объекту и типу доступа, выполненного через разрешение списка контроля доступа (ACL). Наиболее распространенные варианты, чтобы сохранить файл в тайне (ACL зgt; «частный») и сделать его доступным для чтения в Интернете (ACL згт; «общественное чтение»). Поскольку нам нужно будет запросить файл непосредственно у S3, чтобы показать его пользователю, нам нужно ACL

abstract class AWS_S3 {

  // Continued from above...

  protected function get_acl() {

    return 'public-read';
  }
}

Наконец, мы реализуем методы загрузки объекта и загрузки объекта из ведра S3:

abstract class AWS_S3 {

  // Continued from above...

  function upload($file) {

    $s3Client = $this->get_s3_client();

    // Upload a file object to S3
    $s3Client->putObject([
      'ACL' => $this->get_acl(),
      'Bucket' => $this->get_bucket(),
      'Key' => $this->get_file_relative_path($file),
      'SourceFile' => $file,
    ]);
  }

  function download($file) {

    $s3Client = $this->get_s3_client();

    // Download a file object from S3
    $s3Client->getObject([
      'Bucket' => $this->get_bucket(),
      'Key' => $this->get_file_relative_path($file),
      'SaveAs' => $file,
    ]);
  }
}

Затем в детском классе мы определяем название ведра:

class AvatarCropper_AWS_S3 extends AWS_S3 {

  protected function get_bucket() {

    return 'avatars-smashing';
  }
}

Наконец, мы просто мгновенно класс загрузить аватары, или скачать из, S3. Кроме того, при переходе от шагов 1 к 2 и 2 к 3, мы должны сообщить значение $file . Мы можем сделать это, представив поле «file-relative-path» со значением относительного пути $file операции POST (мы не проходим абсолютный путь по соображениям безопасности: нет необходимости включать информацию «/var/www/current» для посторонних):

// Step 1: after the file was uploaded to the server, upload it to S3. Here, $file is known
$avatarcropper = new AvatarCropper_AWS_S3();
$avatarcropper->upload($file);

// Get the file path, and send it to the next step in the POST
$file_relative_path = $avatarcropper->get_file_relative_path($file);
// ...

// --------------------------------------------------

// Step 2: get the $file from the request and download it, manipulate it, and upload it again
$avatarcropper = new AvatarCropper_AWS_S3();
$file_relative_path = $_POST['file_relative_path'];
$file = $avatarcropper->get_file($file_relative_path);
$avatarcropper->download($file);

// Do manipulation of the file
// ...

// Upload the file again to S3
$avatarcropper->upload($file);

// --------------------------------------------------

// Step 3: get the $file from the request and download it, and then save it
$avatarcropper = new AvatarCropper_AWS_S3();
$file_relative_path = $_REQUEST['file_relative_path'];
$file = $avatarcropper->get_file($file_relative_path);
$avatarcropper->download($file);

// Save it, whatever that means
// ...

Отображение файла непосредственно из S3

Если мы хотим отобразить промежуточное состояние файла после манипуляции на шаге 2 (например, пользовательский аватар после обрезания), то мы должны ссылаться на файл непосредственно из S3; URL-адрес не может указать на файл на сервере, так как мы еще раз не знаем, какой сервер будет обрабатывать этот запрос.

Ниже мы добавляем функцию, get_file_url($file) которая получает URL для этого файла в S3. При использовании этой функции, пожалуйста, убедитесь, что ACL загруженных файлов является «общественным чтением», или в противном случае он не будет доступен для пользователя.

abstract class AWS_S3 {

  // Continue from above...

  protected function get_bucket_url() {

    $region = $this->get_region();

    // North Virginia region is simply "s3", the others require the region explicitly
    $prefix = $region == 'us-east-1' ? 's3' : 's3-'.$region;

    // Use the same scheme as the current request
    $scheme = is_ssl() ? 'https' : 'http';

    // Using the bucket name in path scheme
    return $scheme.'://'.$prefix.'.amazonaws.com/'.$this->get_bucket();
  }

  function get_file_url($file) {

    return $this->get_bucket_url().$this->get_file_relative_path($file);
  }
}

Затем мы можем просто получить URL файла на S3 и распечатать изображение:

printf(
  "<img src='%s'>",
  $avatarcropper->get_file_url($file)
);

Файлы листинга

Если в нашем приложении мы хотим разрешить пользователю просматривать все ранее загруженные аватары, мы можем это сделать. Для этого мы вводим функцию, get_file_urls которая перечисляет URL для всех файлов, хранящихся под определенным путем (в терминах S3, это называется префиксом):

abstract class AWS_S3 {

  // Continue from above...

  function get_file_urls($prefix) {

    $s3Client = $this->get_s3_client();

    $result = $s3Client->listObjects(array(
      'Bucket' => $this->get_bucket(),
      'Prefix' => $prefix
    ));

    $file_urls = array();
    if(isset($result['Contents']) && count($result['Contents']) > 0 ) {

      foreach ($result['Contents'] as $obj) {

        // Check that Key is a full file path and not just a "directory"
        if ($obj['Key'] != $prefix) { 

          $file_urls[] = $this->get_bucket_url().$obj['Key'];
        }
      }
    }

    return $file_urls;
  }
}

Затем, если мы храним каждый аватар по пути «/пользователи/пользователи/пользователи/пользователи/», пройдя эту приставку, мы получим список всех файлов:

$user_id = get_current_user_id();
$prefix = "/users/${user_id}/";
foreach ($avatarcropper->get_file_urls($prefix) as $file_url) {
  printf(
    "<img src='%s'>", 
    $file_url
  );
}

Заключение

В этой статье мы рассмотрели, как использовать решение для хранения облачных объектов для использования в качестве общего репозитория для хранения файлов для приложения, развернутого на нескольких серверах. Для решения мы сосредоточились на AWS S3 и продолжили показывать шаги, необходимые для интеграции в приложение: создание ведра, настройку пользовательских разрешений, загрузку и установку SDK. Наконец, мы объяснили, как избежать ловушек безопасности в приложении, и увидели примеры кода, демонстрирующие, как выполнять самые основные операции на S3: загрузка, загрузка и перечисление файлов, которые едва требовали нескольких строк кода каждый. Простота решения показывает, что интеграция облачных сервисов в приложение не является сложной задачей, и она также может быть выполнена разработчиками, которые не имеют большого опыта работы с облаком.

Источник: smashingmagazine.com

Великолепный Журнал

Великолепный, сокрушительный, разящий (см. перевод smashing) независимый журнал о веб-разработке. Основан в 2006 году в Германии. Имеет няшный дизайн и кучу крутых авторов, которых читают 2 млн человек в месяц.

Добавить комментарий

%d такие блоггеры, как: