Как развернуть React приложение с помощью AWS S3 и CloudFront
- воскресенье, 16 октября 2022 г. в 00:39:17
Это первая часть статьи, где мы настроим сервисы 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, для управления доменным именем.
Запрос пользователя попадает в CloudFront (CDN).
Если ответ на представленный запрос уже закэширован в CloudFront, то CloudFront сразу вернет ответ и запрос не пойдет дальше по схеме.
(шаг для Next.js) Запрос перенаправляется в Lambda@Edge (которая также как и CloudFront является Edge сервисом, т.е. распространяется на все точки присутствия сети доставки). В этом сервисе мы определяем какой файл из S3 bucket нам необходимо вернуть в ответ на запрос.
Модифицированный или оригинальный (зависит от предыдущего шага) URL запроса перенаправляется в S3 bucket. Где мы получаем либо файл либо ошибку о том что такого файла не существует.
Файл передается обратно в CloudFront, где кэшируется. Ошибки не кэшируются.
CloudFront возвращает ответ пользователю. В случае ошибки CloudFront делает запрос к S3 bucket на получение заранее определенного файла, например к странице ошибки 404.html.
Предварительно у вас уже должна быть установлена 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>О проекте →</a>
</Link>
</p>
// ...
);
};
// ...
Дальше нам необходимо собрать приложение и сделать экспорт в терминах Next.js или в более общей формулировке: произвести Static Site Generation.
npm run build
# или если вы пользуетесь yarn
yarn build
# экспорт статических файлов
npx next export
После чего в директории out
в качестве артефактов мы будем иметь файлы для нашего многостраничного приложения:
На этом мы заканчиваем работу над приложением.
Как уже было сказано ранее сервис AWS S3 мы будем использовать для хранения файлов нашего приложения.
Для начала работы залогиньтесь в AWS Console и перейдите в раздел Amazon S3.
Далее нажмите на Create bucket
и и укажите имя (Bucket name)
Все остальные настройки можно оставить по умолчанию.
После создания S3 bucket, перейдите в него и загрузите артефакты сборки вашего приложения.
Единственное что нам осталось сделать с S3 это настроить доступ к файлам из других сервисов, но для начала нам необходимо создать такой сервис-потребитель.
Все также оставаясь в 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), следующий раздел вам не нужен, пропустите его.
(для 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 функцию:
// проверим наличие расширения для 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.
Войдите в AWS Console и перейдите в раздел AWS Lambda.
Поскольку Lambda@Edge функции - это функции глобального сервиса CloudFront вам необходимо выбрать основной регион, в AWS это US-East-1 (N. Virginia). Регионы в консоле AWS переключаются в шапке сайта.
Далее нажмите Create function
Выберите создать с нуля Author from scratch
Укажите Имя и Runtime функции (Node.js)
Нажмите Create
После создания функции откроется интерфейс где вы можете редактировать код lambda функции.
вы можете вставить код функции прямо в веб редакторе
или загрузить свою lambda функцию как архив
главное чтобы настройки обработчика совпадали с тем как вы организовали вашу функцию (по умолчанию это index.js файл и именованный экспорт handler):
После сохранения функции необходимо нажать кнопку Deploy
Затем перейдите на вкладку Versions и нажмите Publish new version. Добавьте описание и нажмите Publish. После чего появится версия #1 вашей функции.
Скопируйте Function ARN (адрес, который заканчивается названием вашей функции и ее версией)
На этом создание 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 – это служба системы доменных имен. С помощью этого сервиса можно решить несколько вопросов:
Купить доменное имя, если необходимо (я покажу использование уже существующего доменного имени)
Настроить роутинг доменного имени к нашему 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 запись для вашего домена.
Теперь можно вернуться в консоль Route 53.
Зайдите в раздел Hosted zones, нажмите Create hosted zone. Укажите ваше доменное имя и выберите тип Public hosted zone.
Затем зайдите в созданную зону и создайте A запись для корневого домена вида hostname.com
и CNAME запись для валидации доменного имени в менеджере сертификатов (субдомен и значение для этой записи у каждого уникальное).
Создание A записи
Нажмите Create record
В открывшемся окне оставьте поле subdomain пустым (так значение применится к корневому домену)
Выберите Record type – A
Включите режим Alias (в режиме Alias AWS позволяет ссылаться на внутренние сервисы, это именно тот пункт почему мы не могли воспользоваться сторонним регистратором доменного имени чтобы привязать доменное имя к CloudFront)
В поле Route traffic to найдите CloudFront distribution
В появившемся поле выберите свой distribution
Нажмите Create records
Для CNAME записи все проще – укажите субдомен, выберите тип записи и укажите значение.
Последним небольшим шагом является указание настроек в самом CloudFront.
Мы укажем в нашей дистрибуции доменное имя и сертификат.
Для этого перейдите в консоль CloudFront, выберите дистрибуцию, на вкладке General нажмите кнопку Edit в блоке Settings.
Добавьте альтернативное доменное имя.
Укажите SSL сертификат.
Сохраните изменения
После всех изменений и обновления DNS записей в сети интернет вы сможете использовать свой собственный домен.
Мы развернули статическое клиентское приложение в инфраструктуре Amazon.
Может показаться что процесс настройки Amazon сервисов трудоемкий (учитывая тот факт что мы не рассмотрели процесс автоматизации), однако в результе ваших усилий вы получаете надежную инфраструктуру, готовую масштабироваться под ваши нагрузки.
Если же ваш сайт небольшой вы вполне можете вписаться в бесплатные тарифы для S3, CloudFront и Lambda@Edge.