Как развернуть React приложение с помощью AWS S3 и CloudFront

Это первая часть статьи, где мы настроим сервисы 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, для управления доменным именем.

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

a3f39b85e5bba71e94ed7fbfd4b2020b.webp

  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 вы увидите ваше новое приложение:

0e2354a895b50266944060303397deac.webp

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

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

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

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

const About = () => {
  return (
    

О проекте

); }; export default About;

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

74e2e94ad406b7c7d46f2af4e582385e.webp

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

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

// ...
const Home: NextPage = () => {
  return (
    // ...
    

О проекте →

// ... ); }; // ...

397f31f1584af550a8d14afe9884133d.webp

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

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

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

4404b4ede0062322dc6f5011d869edfd.webp

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

Настройка AWS S3

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

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

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

194fa3e1f5d1fa378499f5a804dd29ec.webp

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

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

486509fe885b345024f73fd13a293497.webp

Единственное что нам осталось сделать с 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, после его создания, чтобы закончить шаги настройки доступа.

0eb47e0886852602e602a4b9b605adb4.webp

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

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

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

8f1e54a410dcbe637fccb99a0ade6dd1.webp

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

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

  • 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 и сохраните изменения.

9ad52a6776d736cf0a9e820970dc6e00.webp

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

0664a2014b721f55400ac6f501937fdd.webp

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

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

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

d37deee83336126c0632bb8b8837116e.webp

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

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

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

2fd034cff17d2fa338d3848c1b35556d.webp

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

676d9d0a1a61c425acb5d7953cac93a1.webp

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

fc400b17a32050b0d6ed5a9d5c3a5d28.webp

Если перейдете по ссылке «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 = ${uri}.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

912da7a79e874e29cbe69f7458cb4c77.webp

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

acd3aa088e4ac4066d56cc73642e6497.webp

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

664ff210b706a24a7b3b5181fab9e92d.webp

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

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

9ff74e278fb972cffc73b0682547a68c.webp

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

abdaca7c4839e7ca3becfc7e9ac606d1.webp

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

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

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

330215ea04750d0186fe3c0fa303b673.webp

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

91c06ce77f42ddd03be531f5eb1cebb8.webp

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

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

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

7101e39f109d55130d36cfb06ad66bae.webp

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

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

a5584d2b3a7a053ad90950bd52c88112.webp

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

1f005d489bee5129ff970152e06c03a1.webp

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

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

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

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

d6323a07b0941c2efcc809aaa39f4f22.webp

Настройка 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 желаемое имя.

2291769db49ffdac3e6e0e0a4f96723c.webp

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

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

Для того чтобы управлять ресурсными записями вашего доменного имени, вам необходимо в интерфейсе регистратора, для вашего имени, прописать 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

8b288a4fa64621f386abf7d7078b6b1d.webp

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

Сертификат

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

ce1189aec1632eb893e8533a2ab0d08d.webp

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

Примечание 1

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

Примечание 2

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

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

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

a969490c79f914c39e173a017fd414f8.webp

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

43902d0d6abbc7ca52ddf03160c40a30.webp

DNS Routing

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

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

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

79d9b00fb9db3970fbf4c01b6a334e28.webp

Создание 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

04d86466b5f998c99bc3c05cfd89aef3.webp

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

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

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

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

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

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

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

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

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

Заключение

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

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

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

© Habrahabr.ru