Это первая часть статьи, где мы настроим сервисы AWS и произведем ручное развертывание приложения. Во второй части мы рассмотрим вопрос автоматизации сборки и доставки.

Важное замечание: клиентская часть нашего сайта будет представлять из себя набор статический файлов, то есть у нас не будет серверного кода во Frontend части.

Вам будет полезна эта статья если вы пишите приложение на базе Create React App или Next.js в режиме Static Export.

Если вы используете Server Side Rendering (SSR) или Incremental Static Regeneration (ISR) – в вашей архитектуре необходимо наличие сервера и как следствие данная статья вам не подходит.

Что мы сделаем

Создадим приложение-пример с использованием фреймворка Next.js, настроим облачные сервисы Amazon (AWS), развернем приложение и настроим маршрутизацию доменного имени.

В данном руководстве мы будем использовать следующие технологии и сервисы:

  • Next.js – мета-фреймворк на базе React.js https://nextjs.org, для создания приложения.

  • Amazon S3 – облачное объектное хранилище https://aws.amazon.com/s3, в качестве веб хостинга.

  • Amazon CloudFront – веб-сервис доставки контента https://aws.amazon.com/cloudfront, в качестве веб сервера и CDN.

  • AWS Lambda@Edge – компонент бессерверный вычислений https://aws.amazon.com/lambda/edge, для обеспечения маршрутизации в многостраничном Next.js приложении.

  • Amazon Route 53 – служба системы доменных имен https://aws.amazon.com/route53, для управления доменным именем.

Как будет выглядеть инфраструктура приложения

  1. Запрос пользователя попадает в CloudFront (CDN).

    • Если ответ на представленный запрос уже закэширован в CloudFront, то CloudFront сразу вернет ответ и запрос не пойдет дальше по схеме.

  2. (шаг для Next.js) Запрос перенаправляется в Lambda@Edge (которая также как и CloudFront является Edge сервисом, т.е. распространяется на все точки присутствия сети доставки). В этом сервисе мы определяем какой файл из S3 bucket нам необходимо вернуть в ответ на запрос.

  3. Модифицированный или оригинальный (зависит от предыдущего шага) URL запроса перенаправляется в S3 bucket. Где мы получаем либо файл либо ошибку о том что такого файла не существует.

  4. Файл передается обратно в CloudFront, где кэшируется. Ошибки не кэшируются.

  5. CloudFront возвращает ответ пользователю. В случае ошибки CloudFront делает запрос к S3 bucket на получение заранее определенного файла, например к странице ошибки 404.html.

Создание Next.js приложения

Предварительно у вас уже должна быть установлена Node.js

Мы будем использовать фреймворк Next.js с Typescript конфигурацией. Чтобы создать приложение наберите в терминале:

npx create-next-app@latest --typescript
# или если вы пользуетесь yarn
yarn create next-app --typescript

? What is your project named? › my-awesome-app
cd my-awesome-app

Чтобы запустить dev сервер с приложением:

npm run dev
# или если вы пользуетесь yarn
yarn dev

Если вы перейдете по адресу http://localhost:3000 вы увидите ваше новое приложение:

Добавим страницы

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

Для этого создадим новый файл  pages/about.tsx, в который добавим следующий код:

// просто воспользуемеся существующими стилями из шаблона
import styles from '../styles/Home.module.css';

const About = () => {
  return (
    <div className={styles.container}>
      <main className={styles.main}>
        <h1 className={styles.title}>О проекте</h1>
      </main>
    </div>
  );
};

export default About;

Теперь по адресу http://localhost:3000/about у вас будет новая страница

Мы также изменим нашу домашнюю страницу, упростив ее и добавив ссылку на страницу /about.

Для этого изменим код в файле pages/index.tsx , попутно уберем лишнее.

// ...
const Home: NextPage = () => {
  return (
    // ...
    <p className={styles.description}>
      <Link href="/about">
        <a>О проекте &rarr;</a>
      </Link>
    </p>
    // ...
  );
};
// ...

Дальше нам необходимо собрать приложение и сделать экспорт в терминах Next.js или в более общей формулировке: произвести Static Site Generation.

npm run build
# или если вы пользуетесь yarn
yarn build
# экспорт статических файлов
npx next export

После чего в директории out в качестве артефактов мы будем иметь файлы для нашего многостраничного приложения:

На этом мы заканчиваем работу над приложением.

Настройка AWS S3

Как уже было сказано ранее сервис AWS S3 мы будем использовать для хранения файлов нашего приложения.

Для начала работы залогиньтесь в AWS Console и перейдите в раздел Amazon S3.

Далее нажмите на Create bucket и и укажите имя (Bucket name)

Все остальные настройки можно оставить по умолчанию.

После создания S3 bucket, перейдите в него и загрузите артефакты сборки вашего приложения.

Единственное что нам осталось сделать с S3 это настроить доступ к файлам из других сервисов, но для начала нам необходимо создать такой сервис-потребитель.

Настройка AWS CloudFront

Все также оставаясь в AWS Console, наберите в поиске CloudFront , перейдите к сервису и в появившемся окне нажмите кнопку Create distribution.

  • В поле Origin domain выберите из списка созданный ранее S3 bucket.

  • В разделе Origin access выберите вариант Origin access control settings. Этот режим позволит нам создать настройки доступа к хранилищу S3 только для одного сервиса, которым будет являться наша CloudFront дистрибуция.

  • Нажмите на кнопку Create control setting для создания настроек доступа. Как указано в информационном сообщении You must update the S3 bucket policy мы еще вернемся в настройки нашего CloudFront Distribution, после его создания, чтобы закончить шаги настройки доступа.

Ниже, в разделе Default cache behavior:

  • Включим автоматическое сжатие данных. CloudFront будет автоматически сжимать определенные файлы, получаемые из источника (в нашем случае S3 bucket), перед их доставкой пользователю. CloudFront сжимает файлы только в том случае, если браузер поддерживает это, как указано в заголовке Accept-Encoding в запросе.

  • Выберем опцию Redirect HTTP to HTTPS для того чтобы доставлять наше приложение только по протоколу HTTPS. И перенаправлять запрос если необходимо.

Все остальные настройки оставим по умолчанию и нажмем кнопку создать.

Теперь настроим страницы, которые мы будем отправлять в качестве ответа по умолчанию:

  • Default root object – будем возвращать index.html

  • Error pages (страницы ошибок) – будем возвращать 404.html для Next.js приложения или все тот же index.html, если ваше приложение создано на базе Create React App, и следовательно имеет только клиентский роутинг.

Для настройки Default root object перейдите на страницу вашего CloudFront Distribution ⇒ General и в разделе Settings нажмите на кнопку Edit. Затем укажите в соответствующем разделе значение index.html и сохраните изменения.

Для настройки страниц ошибок перейдите на вкладку Error pages и нажмите кнопку Create error page response. Создайте конфигурации для ошибок 404 и 403 как указано на рисунке ниже:


Примечание (только для CRA): Если ваше приложение создано с помощью Create React App используйте следующую конфигурацию (для ошибок 404 и 403):

  • Response page path: /index.html

  • HTTP Response code: 200: OK

Ваше приложение будет обрабатывать роутинг непосредственно в клиентском коде в браузере.


Если после всех настроек подождать когда CloudFront Distribution будет развернут, то перейдя по ссылке Distribution domain name вы увидите сообщение «В доступе отказано».

Чтобы разрешить доступ к файлам S3 bucket из нашего CloudFront distribution добавим настройки доступа.

Для этого зайдем CloudFront ⇒ Distributions ⇒ Ваша дистрибуция, во вкладке Origins выберем наш S3 origin и нажмем Edit.

В открывшемся окне скопируем политику доступа Origin access ⇒ Copy policy и перейдем к настройкам S3 bucket Go to S3 bucket permissions.

Далее отредактируем политики доступа для S3 bucket Bucket policy ⇒ Edit. Вставим скопированную политику (иногда необходимо убрать лишние пробелы в JSON, это можно сделать в редакторе кода с помощью Prettier).

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

Если перейдете по ссылке «O проекте» увидите страницу /about – клиентский роутинг будет работать.

Если вы перезагрузите страницу about или зайдете по прямой ссылке /about – то увидите страницу ошибки 404. Однако если зайдете по ссылке /about.html – снова увидите страницу about.

Вы возможно уже догадались что нам необходимо маршрутизировать запросы вида about в /about.html.

Вышесказанное касается только статических многостраничных сайтов, если же вы используете Create React App или другого рода Single Page Application (SPA), следующий раздел вам не нужен, пропустите его.

Создание AWS Lambda@Edge

(для Next.js приложения)

Как уже было сказано, AWS Lambda@Edge это компонент бессерверный вычислений, который, как видно из названия, является Edge сервисом.

Lambda@Edge функции привязаны к CloudFront дистрибуции и распространяются на все регионы и точки присутствия вашего CloudFront.

Это означает что обрабатываемому в Lambda@Edge запросу не нужно ходить в географически удаленный от CDN регион, и как следствие запрос может быть обработан очень быстро.

Статический маршруты

В предыдущем шаге мы выяснили что для web страниц Next.js фреймворк генерирует html файлы. Поэтому для каждого запроса к странице нам необходимо динамически подменить адрес назначение (URI):

/about => /about.html
/posts/how-to-deploy-react-app => /posts/how-to-deploy-react-app.html
...

Amazon AWS поддерживает среду выполнения Node.js для своих lambda функций, поэтому все что нам необходимо, это создать простую Javascript функцию подмены адреса.

Документация по Lambda@Edge

Создадим следующую lambda функцию:

// проверим наличие расширения для JS, CSS, IMG файлов
const hasExtension = /(.+).[a-zA-Z0-9]{2,5}$/;

// для index страницы нам нет необходимости подменять uri, так как
// это наша страница «По умолчанию» (см. Default root object в статье выше)
const isIndex = (uri) => uri === '/';

// lambda функция ожидает именованный экспорт функции handler
exports.handler = function (event, _ctx, callback) {
  const request = event.Records[0].cf.request;
  const uri = request.uri;

  if (uri && !isIndex(uri) && !hasExtension.test(uri)) {
    // подменяем uri для адресов страниц
    request.uri = <span class="hljs-subst" style="box-sizing: border-box; border: 0px solid rgb(229, 231, 235); --tw-border-spacing-x:0; --tw-border-spacing-y:0; --tw-translate-x:0; --tw-translate-y:0; --tw-rotate:0; --tw-skew-x:0; --tw-skew-y:0; --tw-scale-x:1; --tw-scale-y:1; --tw-pan-x: ; --tw-pan-y: ; --tw-pinch-zoom: ; --tw-scroll-snap-strictness:proximity; --tw-ordinal: ; --tw-slashed-zero: ; --tw-numeric-figure: ; --tw-numeric-spacing: ; --tw-numeric-fraction: ; --tw-ring-inset: ; --tw-ring-offset-width:0px; --tw-ring-offset-color:#fff; --tw-ring-color:rgba(59,130,246,0.5); --tw-ring-offset-shadow:0 0 #0000; --tw-ring-shadow:0 0 #0000; --tw-shadow:0 0 #0000; --tw-shadow-colored:0 0 #0000; --tw-blur: ; --tw-brightness: ; --tw-contrast: ; --tw-grayscale: ; --tw-hue-rotate: ; --tw-invert: ; --tw-saturate: ; --tw-sepia: ; --tw-drop-shadow: ; --tw-backdrop-blur: ; --tw-backdrop-brightness: ; --tw-backdrop-contrast: ; --tw-backdrop-grayscale: ; --tw-backdrop-hue-rotate: ; --tw-backdrop-invert: ; --tw-backdrop-opacity: ; --tw-backdrop-saturate: ; --tw-backdrop-sepia: ; color: rgb(77, 77, 76);">${uri}</span>.html;
  }

  return callback(null, request);
};

Lambda функция принимает 3 параметра:

  • первый параметр - event - объект события, который содержит информацию от вызывающего компонента (зависит от AWS сервиса). В нашем случае это CloudFront событие. Подробнее здесь.

// Пример CloudFront message event
{
  "Records": [
    {
      "cf": {
        "config": {
          "distributionId": "EDFDVBD6EXAMPLE"
        },
        "request": {
          "clientIp": "xxxx:xxxx:xxxx:x:x:xxxx:xxxx:xxxx",
          "method": "GET",
          "uri": "/about",
          "headers": {
            "host": [
              {
                "key": "Host",
                "value": "xxxx.cloudfront.net"
              }
            ],
            "user-agent": [
              {
                "key": "User-Agent",
                "value": "curl/7.51.0"
              }
            ]
          }
        }
      }
    }
  ]
}

  • второй параметр - context - объект, который содержит информацию о вызове, функции и среде выполнения. Подробнее здесь.

  • третий параметр - callback - это функция, которую вы можете вызывать в синхронных обработчиках для отправки ответа. Функция обратного вызова принимает два аргумента: ошибку и ответ. Когда вы вызываете callback, Lambda ждет, пока цикл обработки событий станет пустым, а затем возвращает ответ или ошибку инициатору. Объект ответа должен быть совместим с JSON.stringify. Для асинхронных обработчиков, вместо вызова callback функции, вы должны вернуть ответ, ошибку или Promise. Смотри примеры здесь.

Динамические маршруты

Возможно в своем приложении вы захотите использовать динамические маршруты. В таком случае вы можете добавить еще одну проверку следующим образом:

// ...
const isPost = (uri) => uri.startsWith('/posts/');
const POST_ROUTE = '/posts/[slug].html';

exports.handler = function (event, _ctx, callback) {
  // ...
  if (uri && !isIndex(uri) && !hasExtension.test(uri)) {
    request.uri = isPost(uri) ? POST_ROUTE : `${uri}.html`;
  }
  // ...
};

Примечание

Хотя выше описанный подход и является рабочим, если вы планируете делать сложный роутинг с динамическими маршрутами без создания статических страниц на этапе сборки, вы оказываетесь в ситуации когда роутинг вашего приложения должен быть и в приложении и в Lambda функции (напомню что Lambda функция - это часть инфраструктуры). Поддерживать такое решение трудозатратно и не удобно, по этому вам стоит рассмотреть вариант с развертыванием Frontend сервера, который будет поддерживать все возможности Next.js приложения, включая SSR, ISR и On-demand Revalidation.

Развертывание Lambda@Edge функции

  1. Войдите в AWS Console и перейдите в раздел AWS Lambda.

Поскольку Lambda@Edge функции - это функции глобального сервиса CloudFront вам необходимо выбрать основной регион, в AWS это US-East-1 (N. Virginia). Регионы в консоле AWS переключаются в шапке сайта.

  1. Далее нажмите Create function

  2. Выберите создать с нуля Author from scratch

  3. Укажите Имя и Runtime функции (Node.js)

  4. Нажмите Create

  1. После создания функции откроется интерфейс где вы можете редактировать код lambda функции.

  • вы можете вставить код функции прямо в веб редакторе

  • или загрузить свою lambda функцию как архив

главное чтобы настройки обработчика совпадали с тем как вы организовали вашу функцию (по умолчанию это index.js файл и именованный экспорт handler):

  1. После сохранения функции необходимо нажать кнопку Deploy

  2. Затем перейдите на вкладку Versions и нажмите Publish new version. Добавьте описание и нажмите Publish. После чего появится версия #1 вашей функции.

  1. Скопируйте Function ARN (адрес, который заканчивается названием вашей функции и ее версией)

  1. На этом создание Lambda функции завершено.

Теперь нам осталось связать Lambda функцию с CloudFront дистрибуцией.

Для этого перейдите в AWS Console в консоль CloudFront ⇒ Distribution ⇒ ID. На вкладке Behaviors выберите Default поведение и нажмите Edit.

В открывшемся окне в блоке Function associations для Origin request события укажите ARN вашей lambda функции и включите передачу body. Нажмите Save changes.

Origin request - событие или хук, которое позволяет запускать lambda функцию перед тем как запрос будет перенаправлен из CloudFront в Origin (компонент на который ссылается CloudFront дистрибуция). Как вы можете помнить в нашем случае мы имеем один Origin и им ****является S3 bucket с файлами нашего сайта.

Возможные проблемы

Если при попытке добавить Lambda функцию к CloudFront дистрибуции вы столкнулись со следующей ошибкой:

То вам понадобится настроить Execution role для Lambda функции.

Для этого вернитесь в консоль Lambda функций, выберите вашу функцию и на вкладке Configuration перейдите в раздел Permissions. В блоке Execution role перейдите по ссылке Role name.

В открывшемся окне на вкладке Trust relationships добавьте edgelambda.amazonaws.com в качестве допустимых сервисов.

После чего повторите процедуру добавление Lambda функции в качестве Origin request для CloudFront дистрибуции.

Проверка результатов

После завершения развертывания изменений в CloudFront вы можете проверить как работает ваш роутинг.

Теперь если вы зайдете по прямой ссылке на страницу /about вы увидите страницу нашего сайта, а не ошибку как было до этого.

Настройка AWS Route 53 и Certificate Manager

AWS Route 53 – это служба системы доменных имен. С помощью этого сервиса можно решить несколько вопросов:

  • Купить доменное имя, если необходимо (я покажу использование уже существующего доменного имени)

  • Настроить роутинг доменного имени к нашему CloudFront distribution.

С помощью сервиса Certificate Manager можно выпустить или импортировать SSL/TLS сертификат для работы HTTPS доступа к вашему сайту.

Доменное имя

Итак если вы еще не приобрели доменное имя, это легко можно сделать через Route 53.

Для этого в AWS Console находим сервис Router 53 и вбиваем в поле Find and register an available domain желаемое имя.

Далее следуйте шагам по выбору домена, добавлению в корзину и оплате, эти шаги я опущу.

Если у вас есть доменное имя, купленное у другого регистратора

Для того чтобы управлять ресурсными записями вашего доменного имени, вам необходимо в интерфейсе регистратора, для вашего имени, прописать DNS-серверы Amazon.

Почему нельзя настроить роутинг доменного имени просто через интерфейс другого регистратора (не Amazon)?

Дело в том что AWS CloudFront имеет динамический пул адресов и вы не сможете создать A-запись для своего домена ни у одного регистратора, кроме Amazon Route 53.

Итак, как передать управление ресурсными записями на примере reg.ru.

Необходимо перейти к управлению зоной и указываете свои собственные DNS-серверы. На текущий момент Amazon владеет следующими DNS серверами:

ns-1458.awsdns-54.org
ns-384.awsdns-48.com
ns-527.awsdns-01.net
ns-1816.awsdns-35.co.uk

После применения изменений, обновление DNS записей может занять от нескольких часов до нескольких дней.

Сертификат

Для создания сертификата перейдите в AWS Certificate Manager. Для этого наберите в AWS Console наберите в поиске Certificate Manager.

Здесь вы можете запросить или импортировать имеющийся сертификат.

Примечание 1

CloudFront поддерживает только 1024-битные и 2048-битные ключи RSA. То есть RSA-2048 это максимум что вы сможете использовать с CloudFront, хотя сам Certificate Manager поддерживает и большие ключи.

Примечание 2

Создавать или импортировать сертификат необходимо находясь в регионе US-East-1 (N. Virginia). Правило такое же как и для Lambda@Edge – мы хотим подключить сертификат к глобальному сервису CloudFront, управление к которому обеспечивается через основной AWS регион. Регионы в консоле AWS переключаются в шапке сайта.

Так что если у вас уже есть сертификат, но он использует больший ключ, просто создайте новый сертификат в Amazon.

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

Если вы выберите валидацию через DNS то для подтверждения вам необходимо будет добавить CNAME запись для вашего домена.

DNS Routing

Теперь можно вернуться в консоль Route 53.

Зайдите в раздел Hosted zones, нажмите Create hosted zone. Укажите ваше доменное имя и выберите тип Public hosted zone.

Затем зайдите в созданную зону и создайте A запись для корневого домена вида hostname.com и CNAME запись для валидации доменного имени в менеджере сертификатов (субдомен и значение для этой записи у каждого уникальное).

Создание A записи

  1. Нажмите Create record

  2. В открывшемся окне оставьте поле subdomain пустым (так значение применится к корневому домену)

  3. Выберите Record type – A

  4. Включите режим Alias (в режиме Alias AWS позволяет ссылаться на внутренние сервисы, это именно тот пункт почему мы не могли воспользоваться сторонним регистратором доменного имени чтобы привязать доменное имя к CloudFront)

  5. В поле Route traffic to найдите CloudFront distribution

  6. В появившемся поле выберите свой distribution

  7. Нажмите Create records

Для CNAME записи все проще – укажите субдомен, выберите тип записи и укажите значение.

Связывание настроек с CloudFront

Последним небольшим шагом является указание настроек в самом CloudFront.

Мы укажем в нашей дистрибуции доменное имя и сертификат.

Для этого перейдите в консоль CloudFront, выберите дистрибуцию, на вкладке General нажмите кнопку Edit в блоке Settings.

  1. Добавьте альтернативное доменное имя.

  2. Укажите SSL сертификат.

  3. Сохраните изменения

После всех изменений и обновления DNS записей в сети интернет вы сможете использовать свой собственный домен.

Заключение

Мы развернули статическое клиентское приложение в инфраструктуре Amazon.

Может показаться что процесс настройки Amazon сервисов трудоемкий (учитывая тот факт что мы не рассмотрели процесс автоматизации), однако в результе ваших усилий вы получаете надежную инфраструктуру, готовую масштабироваться под ваши нагрузки.

Если же ваш сайт небольшой вы вполне можете вписаться в бесплатные тарифы для S3, CloudFront и Lambda@Edge.

Комментарии (1)


  1. ArkadiyShuvaev
    15.10.2022 21:36

    Что касается холодного старта Lambda. Какое среднее значение было в вашем случае?

    Не получится ли так, что response time будет уже не 10 -15-20 мс, а все 100 - 300?