Это первая часть статьи, где мы настроим сервисы 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.
Создание 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>О проекте →</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 функцию:
// проверим наличие расширения для 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 функции
Войдите в 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 и 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 записи
Нажмите Create record
В открывшемся окне оставьте поле subdomain пустым (так значение применится к корневому домену)
Выберите Record type – A
Включите режим Alias (в режиме Alias AWS позволяет ссылаться на внутренние сервисы, это именно тот пункт почему мы не могли воспользоваться сторонним регистратором доменного имени чтобы привязать доменное имя к CloudFront)
В поле Route traffic to найдите CloudFront distribution
В появившемся поле выберите свой distribution
Нажмите Create records
Для CNAME записи все проще – укажите субдомен, выберите тип записи и укажите значение.
Связывание настроек с CloudFront
Последним небольшим шагом является указание настроек в самом CloudFront.
Мы укажем в нашей дистрибуции доменное имя и сертификат.
Для этого перейдите в консоль CloudFront, выберите дистрибуцию, на вкладке General нажмите кнопку Edit в блоке Settings.
-
Добавьте альтернативное доменное имя.
-
Укажите SSL сертификат.
Сохраните изменения
После всех изменений и обновления DNS записей в сети интернет вы сможете использовать свой собственный домен.
Заключение
Мы развернули статическое клиентское приложение в инфраструктуре Amazon.
Может показаться что процесс настройки Amazon сервисов трудоемкий (учитывая тот факт что мы не рассмотрели процесс автоматизации), однако в результе ваших усилий вы получаете надежную инфраструктуру, готовую масштабироваться под ваши нагрузки.
Если же ваш сайт небольшой вы вполне можете вписаться в бесплатные тарифы для S3, CloudFront и Lambda@Edge.
ArkadiyShuvaev
Что касается холодного старта Lambda. Какое среднее значение было в вашем случае?
Не получится ли так, что response time будет уже не 10 -15-20 мс, а все 100 - 300?