- Введение
- Философия REGAL 5
- Быстрая итерация
- Корректность
- Строго типизированный функционал
- Бессерверный первый
- CICD на основе магистрали
- Выбор технологического стека REGAL
- GraphQL
- AWS AppSync
- AWS Amplify
- AWS Lambda
- ReScript
- Elm
- Сплошная архитектура
- Моделирование домена
- Создание и развертывание BFF
- Сборка и развертывание пользовательского интерфейса
- Изменение схемы
- Развертывание
- Откат
- Опции
- Monorepo
- Route53 против CloudFlare
- Только запросы AppSync
- Elm GraphQL Type Mapping
- Сравнение других технических стеков
- ReScript React
- Phoenix Framework
- Svelte/Vue/Angular
- Будущие улучшения
- Улучшения монорепо
- Code Deploy Cypress Mystery Failures
- Смена языка R с ReScript на Roc-Lang
- ReScript GraphQL Generator
- Выводы
Введение
Архитектура REGAL — это технологический стек, созданный с использованием лучших отраслевых практик и современных инструментов для создания веб-приложений. Если вы хотите построить, развернуть и разместить веб-приложение полного стека с минимальным количеством исключений во время выполнения, минимальным временем простоя и уверенной, но достаточно быстрой итерацией, REGAL для вас. Здесь ничего не изобретается, это просто комбинация существующих технологий, объединенных в единый технологический стек.
REGAL обозначает основные технологии, используемые в технологическом стеке:
- R: ReScript
- E: Elm
- G: GraphQL
- A: AWS Amplify и AppSync
- L: AWS Lambda
Это ранние дни, но мы успешно развернули 2 приложения в продакшн на моей работе, поэтому есть достаточно информации и повторяющихся паттернов, которые помогут другим командам. Кроме того, на подходе несколько интересных технологий, которые могут улучшить архитектуру в будущем.
В этом посте мы рассмотрим:
- что представляет собой стек REGAL
- почему была выбрана каждая технология
- как каждая из них сочетается в целостную архитектуру
- какие у вас есть возможности для изменения определенных частей и возможные эффекты «за» и «против
- сравнение с другими технологическими стеками, чтобы помочь с контекстом & понимание опций
- Будущие улучшения
Хотя вы можете не принять или даже согласиться со всем стеком, я гарантирую, что вы найдете в нем какую-то технологию или практику, которую вы можете использовать для улучшения ваших текущих программных проектов.
Философия REGAL 5
Несмотря на то, что вы читаете в социальных сетях, блогах и книгах о том, что программирование пытается быть похожим на инженерию, на самом деле программирование — это искусство. Поэтому даже у самых талантливых программистов, которых я встречал, есть философия подхода к программированию, которая определяет все их технические решения. Эти убеждения также могут меняться со временем. У нас есть 8 000+ языков программирования, существующие языки модифицируются, и постоянно создаются новые. Люди хотят творить так, как им удобно. Ключевое слово «чувствует». Когда мы меняемся, меняется программирование, а оно, в свою очередь, меняет нас, и цикл повторяется.
Хотя качественные данные являются законными, мы также должны использовать строгий, научный инженерный подход к тому, что мы делаем, насколько это возможно. В программировании трудно получить хорошие данные и получить их подтверждение. Это означает, что не так много исследований, которые были бы достаточно убедительными и подтвержденными, чтобы каждый в сообществе программистов сказал: «О, это факт, мы должны это сделать». Но у нас есть те, кто проводит хорошие исследования и делится этими данными. Мы должны стараться использовать все, что можем.
REGAL сочетает в себе как мою философию программирования, так и лучшие отраслевые практики, основанные на имеющихся у нас данных. Вот они, по порядку:
- Быстрая итерация: Скорость — это ключ
- Корректность: Код при компиляции должен быть настолько корректным, насколько это возможно.
- Строго типизированное функциональное программирование: мы практикуем строго типизированное функциональное программирование и применяем это мышление в наших архитектурах в отличие от процедурного, объектно-ориентированного и отсутствия типов.
- Serverless First: мы создаем архитектуру с учетом бессерверной архитектуры, а не архитектуры с полным штатом и серверами.
- CICD на основе магистрали: мы верим в непрерывную интеграцию на основе магистрали с интенсивным автоматизированным тестированием, чтобы гарантировать, что мы можем непрерывно и уверенно поставлять продукты в производство несколько раз в день.
Давайте рассмотрим каждую концепцию и то, как она применяется к выбранным и не выбранным технологиям.
Быстрая итерация
Один из самых эффективных способов разработки программного обеспечения — это быстрые циклы обратной связи. Напишите код, протестируйте его, изучите, повторите. Чем медленнее вы видите, как 1 строка кода работает как локально, так и в среде разработки, тем хуже становится ваша способность писать хороший код. «Обратная связь» хорошо обсуждается здесь, в статье Тима Кохрана о максимизации эффективности разработчика от Thoughtworks.
Ничто не делает кодовую базу более ужасной для работы, чем медленный цикл обратной связи, независимо от любых других положительных факторов. Поэтому я считаю его самым важным, и поэтому он стоит под номером 1.
Функциональные языки, такие как Haskell, Rust, Elixir и Scala, соответствуют принципам корректности и функциональности. Однако время сборки и методология развертывания у них медленные. Технически они «быстрые», если вы говорите с разработчиком, имеющим опыт работы с этими технологиями, но для меня они слишком медленные. Haskell и Rust имеют печально известное медленное время компиляции. Очень часто все 4 языка развертываются на серверах в отличие от безсерверных архитектур, где часто используется Docker, который также медленный. Использование таких языков, как TypeScript, в функциональном ключе возможно, но компиляция TypeScript, как известно, медленная на больших базах кода или при использовании типов
над интерфейсами
, отсюда рост нативных альтернатив, таких как Vite, или встроенной поддержки, такой как Deno и Bun.
В ReScript используется OCAML, который является самым быстрым компилятором в мире. Он также имеет опыт позитивного влияния на сообщества, как, например, компилятор MTASC в ActionScript прошлых лет. При условии, что все ваши функции в конечном итоге имеют определения типов, Elm также работает практически мгновенно. Хотя CloudFormation, как известно, работает медленно, вы можете использовать AWS SDK, будь то различные фреймворки, такие как deploy function
от Serverless или Accelerate от AWS SAM, чтобы развернуть ваш код в среде QA за считанные секунды. Время выхода на рабочий режим новых функций Lambda в конечном итоге также составляет от нескольких секунд до нескольких мгновений. Это включает привязку к существующей настройке AppSync. Наконец, кэширование Amplify для развертывания нового изменения пользовательского интерфейса также практически мгновенно.
На раскрутку EC2 уходят минуты, ECS/EKS требуется много минут, чтобы привести новые контейнеры/поды в рабочее состояние, а откат также происходит медленно.
Наконец, использование Node.js с ограниченным количеством библиотек означает, что артефакты развертывания Lambda будут небольшими, быстро развертываться и не замедлять запуск Lambda. Признаться, в экосистеме Node.js это сделать сложнее, поскольку библиотеки — это одновременно и сильная (их много), и слабая сторона (вы склонны использовать их много).
Корректность
В основном существует 3 типа ошибок:
- синтаксис
- нулевые указатели
- логические
Синтаксические ошибки печально известны в динамических языках, поскольку там нет компилятора. Таким образом, вы пишете код, не имея представления о том, правильный ли он, и просто запускаете его, чтобы проверить, так ли это. Плюсом здесь является то, что динамические языки имеют тенденцию быть быстрыми, поэтому этот процесс «работает ли это? работает ли это? работает ли это? работает ли это?» может выполняться много раз в минуту. Однако по мере роста кодовой базы этот процесс становится утомительным и чреватым ошибками. Многие языки имеют инструменты линтинга, помогающие в этом, например, ESLint для JavaScript и Black для Python, что усложняет инструментарий, но при этом вы можете сохранить ту же скорость итераций.
Если ваш код имеет определенную форму, вам может помочь компилятор. Такие вещи, как Haskell, Scala/Gleam, TypeScript, Python Typings with mypy и Rust имеют действительно хорошие компиляторы, которые помогут вам «приблизиться» к корректности. Это означает, что при выполнении кода у вас больше не будет синтаксических ошибок.
Однако с нулевыми указателями дело обстоит сложнее. Для большинства языков это просто статус-кво, и многие либо даже не пытаются (Python/JavaScript/Lua), либо делают это вашей проблемой (Go, Java, C#), либо пытаются предоставить альтернативы null (OCAML, F#, ReScript, Rust), либо просто не включают их в язык, делая их невозможными (Elm, Haskell). Вы получаете гарантию корректности только тогда, когда компилируете код и он работает. Нулевые указатели, удивляющие вас, не учитываются в нашей философии работы и корректности.
Elm предлагает это для пользовательского интерфейса. ReScript не предлагает этого для сервера, но близок к этому, и гораздо безопаснее, чем TypeScript/Python/Elixir. Rust может работать, но мы создаем BFF или бессерверные архитектуры, не требующие низкоуровневых возможностей, которые предоставляет Rust. Лучше использовать Haskell или Roc. Подробнее о том, почему они не были выбраны, мы расскажем позже.
Наконец, если убрать синтаксические ошибки и нулевые указатели, останутся только логические ошибки. Примеры: «Если мы прилетаем в 17:00, а посадка на самолет заканчивается в 17:00, почему функция говорит, что мы опоздали?». Это происходит потому, что функция не включает инклюзивную нумерацию; она использует больше чем (>) вместо больше чем или равно (>=). Простая ошибка, но все приложение не имеет синтаксических ошибок, нулевых указателей, но при этом работает некорректно. Это то, на что мы должны тратить свое время, тестируя, как автоматизированно, так и вручную, потому что это самое сложное, что можно исправить, и то, что мы надеемся часто итерировать, чтобы стать лучше. Elm удовлетворяет этому, а ReScript в основном удовлетворяет.
Помните, независимо от инструментария, кодовая база корректна лишь настолько, насколько программист способен сформулировать, что значит «корректно», а мы ужасны в этом. Лучше всего сосредоточиться на этих проблемах, и позволить инструментарию исправить две другие (синтаксис и нулевые указатели), чтобы они больше не были проблемой, и мы могли сосредоточиться на важных вещах.
Строго типизированный функционал
Чистые функции предсказуемы; они либо работают, либо нет, и их легко тестировать. Нам нравятся такие функции, и мы любим тестирование.
Неизменяемые данные предсказуемы, устраняют целый ряд ошибок, связанных с мутацией, и соответствуют тому, как вы создаете архитектуру в бессерверных/реактивных архитектурах. Нам нравится это соответствие.
Типы позволяют устранить целый ряд распространенных ошибок и предоставляют язык для моделирования вещей, учитывая, что в функциональном программировании нет классов. Функциональные типы, однако, с большей вероятностью обеспечивают гарантии корректности по сравнению с такими типами, как в Go, Java, C# или TypeScript. Нам нравятся никогда не возникающие ошибки и моделирование данных.
Очевидно, что Go, Java, C#, Python и TypeScript явно не подходят, поскольку основаны на разрешении таких вещей, как Object и null-указатели. В TypeScript есть несколько замечательных функциональных типов, но в языке слишком много неявных аварийных люков, что приводит к тому, что вещи становятся небезопасными. TypeScript также сделал замечательный шаг вперед в поддержке типов записей, а также типов сумм с разумной проверкой строгости, когда вы используете их в операторе switch со строгим включением. Однако в целом TypeScript очень ориентирован на ООП, а типы довольно многословны для базового моделирования.
ReScript, Elm, Rust, Haskell, Idris и Scala, использующие что-то вроде Zio/Cats/Salaz, предлагают замечательные возможности звуковой (в отличие от строгой) типизации. Elixir функционален, но не имеет типов (пока, есть предложение по этому поводу), а Gleam имеет. F# и OCAML имеют чрезвычайно хорошие типы, но все еще позволяют классы, нулевые указатели и исключения «если вы хотите». bruh. К сожалению, так же как и ReScript.
Elm и ReScript, а также GraphQL позволяют использовать чрезвычайно похожий синтаксис для определения типов записей (and) и сумм (or). Это означает, что ваши типы безопасны, а также читаемы на всех уровнях технологического стека с меньшим количеством переключений контекста. Помните, что типы — это еще и нагрузка.
Нам нравится «когда это компилируется, это работает». Под «работает» мы подразумеваем, что любые возникающие ошибки — это логические ошибки, а не синтаксис или нулевые указатели, которых можно избежать.
Бессерверный первый
Вам либо нравится возиться с инфраструктурой, либо нет. Мне — нет. Мне нравится передавать время безотказной работы AWS, чтобы я мог сосредоточиться на кодинге. Чтобы извлечь из этого максимальную выгоду, нужно изучить, как лучше всего строить архитектуру с использованием бессерверных технологий. Основная философия REGAL: «Вы развертываете систему на AWS, чтобы она могла управлять вашим пользовательским интерфейсом и API».
CICD на основе магистрали
Feature Branches работает для распределенных команд, которые не знают и/или не доверяют друг другу. Trunk-based работает для тех, кто знает и доверяет друг другу. PR замедляет создание кода и создает много источников правды. Trunk-based означает, что вы ни от кого не ждете, контроль исходников git облегчает слияние, и у всех разработчиков есть один источник правды о коде.
Это также означает, что вы должны делать много частых коммитов в день. Компиляторы Elm & ReScript, хотя и быстрые и хорошие, оба поощряют небольшие, инкрементные изменения. Elm популяризировал бесстрашный рефакторинг, а ReScript, безусловно, близок к этому. Объедините это с автоматизированными модульными, интеграционными и конечными тестами в вашем конвейере, и вы получите более уверенный способ дойти до производства много раз в день. Такие практики, как Test Driven Development и парное программирование, поощряются, но не являются обязательными.
Предполагается, что основы CICD соблюдаются:
- много коммитов разработчиком в день
- все проверки качества кода автоматизированы
- это включает в себя линтинг, тестирование, безопасность и развертывание в различных средах
- откат и/или зеленые/синие или канареечные сборки должны быть простыми, нажатием одной кнопки
- разработчики постоянно объединяют и продвигают код, а основная ветка является источником истины
- ничего страшного, если для запуска в производство требуется ручное утверждение.
- Нет никаких запросов на рецензирование/слияние; код постоянно проверяется разработчиками самостоятельно, а также в ходе парного программирования и моб-сессий.
Выбор технологического стека REGAL
Пойдем в порядке убывания важности по сравнению с буквенным положением.
GraphQL
Существует множество вариантов создания клиент-серверных архитектур. REST, gRPC и т.д. GraphQL был выбран по следующим причинам:
- он использует современную систему типизации, состоящую из типов ands (записи) и ors (перечисления & объединения), что позволяет функционально моделировать богатый домен.
- Моделирование бизнес-домена с использованием реального языка бизнеса заимствует идеи из Domain Driven Design (не большое количество кода и абстракции), чтобы соответствовать философии «Правильности». Должен быть один источник истины для того, «что есть вещь», и использование GraphQL обеспечивает согласие клиента и сервера.
- Моделирование домена важно, но также важны ошибки проверки типов в компиляторе и во время выполнения. GraphQL гарантирует, что типы на клиенте такие же, как и на сервере. Многие ошибки легко совершить, когда вы выходите «за пределы своей системы типов», и REST печально известен этим. GraphQL гарантирует, что типы будут одинаковыми, будь то на клиенте, на сервере или по проводам.
- AWS предлагает управляемый хостинг GraphQL, который соответствует философии Serverless First.
- Он находится на вершине REST, поэтому мы все еще можем использовать инструменты REST на базе браузера для отладки некоторых наших запросов. GraphQL легче читать, чем закодированный Protobuf.
AWS AppSync
AWS AppSync — это управляемый хостинг GraphQL API. Вместо того, чтобы писать какой-то сервер типа Apollo, затем упаковывать его в контейнер и развертывать на ECS/K8’s, вы можете просто «позволить AWS все это обработать».
AppSync делает многое, но наиболее ценными являются следующие вещи:
- бессерверный GraphQL-сервер
- размещает вашу схему и проверяет ее на входящие и исходящие запросы с распространенными ошибками (не требуется никакого кода, это просто работает)
- предоставляет 3 основные формы аутентификации, включая токен, «что скажет ваша Lambda» и JSON Web Token с автоматическим разбором заголовка авторизации
- опции для связи запросов и мутаций вашей схемы с Lambda(s)
- опции для связи отдельных полей вашей схемы с различными источниками данных, включая Lambda, DynamoDB, HTTP и другие.
- Автоматически включает распределение CloudFront с дополнительной интеграцией Route53.
- Все Lambda получают достаточно информации о запросе в своем событии Object, чтобы определить любой необходимый контекст о запросе, как ответить и с какими данными.
- Встроенное кэширование данных или базы сессий с возможностью аннулирования.
Отсутствие необходимости размещать и нянчиться с сервером, вручную подключать CloudFront/CloudFlare, писать различные коды аутентификации и маршрутизации в Apollo — это потрясающе. Еще более удивительно то, что он управляется AWS и по умолчанию является бессерверным. Это действительно BFF (back-end для front-end) будущего.
AWS Amplify
Если у вас есть BFF, значит, у вас есть front-end. Если у вас есть front-end, вам нужно где-то его разместить. Amplify — это управляемая служба AWS, созданная для размещения одностраничных приложений. Он абстрагирует все существующие бессерверные технологии, которые вы традиционно используете на AWS, в одном месте, автоматизируя большинство из них. Статические активы S3, разрушение кэша при развертывании, и даже собственный конвейер сборки с использованием CodeDeploy, взятый прямо из вашего репозитория кода. Как и AppSync, он создает для вас дистрибутив CloudFront и, по желанию, обеспечивает автоматическое создание Route53, если вам нужен полный URL.
Есть и другие различные функции, но единственная, которая действительно имеет значение, это «мне нужно разместить сайт на AWS и я хочу, чтобы его было легко настроить». Если вы когда-либо достигали предела публичных активов на S3 и были вынуждены обращаться к ALB и EC2… Amplify — это огромный глоток свежего воздуха.
Если вы не работаете в строго регулируемой среде, Amplify CLI позволяет вам создать монорепо как Amplify, так и AppSync с помощью командной строки.
Некоторые предостережения по CodeDeploy & AppSync приведены ниже.
AWS Lambda
Следуя философии Serverless First в сочетании с функциональным мышлением, Lambda удовлетворяет обоим требованиям. AWS Lambda управляет вашим кодом; вы просто поворачиваете кучу ручек масштабирования и конфигурации. Никаких серверов, с которыми нужно нянчиться, никаких контейнеров, которыми нужно управлять, только код.
И под «просто кодом» мы подразумеваем «просто функции». Функциональное программирование — это поклонение «церкви чистых функций», и мы делаем все возможное, чтобы наши Lambda имели функциональное ядро с императивной оболочкой. Контракт AWS Lambda выглядит следующим образом:
- на входе — любой триггер (в данном случае, чаще всего AppSync передает ему GraphQL-запрос)
- выходом является либо ответ типа GraphQL-запроса, либо ошибка(и), либо и то, и другое.
- Исключение
AWS обрабатывает ошибки времени выполнения по-разному в зависимости от триггера, но в большинстве сервисов они поощряются (AppSync здесь является своего рода исключением из правил). Традиционно такие сервисы, как SQS, SNS или Kinesis, используют исключение времени выполнения в качестве сигнала для повторной попытки. API-шлюзы/ALB используют их как сигнал для ответа 500. Шаговые функции имеют наибольшую свободу.
AppSync, однако, интересен тем, что в конечном итоге он станет HTTP-ответом, поэтому вы, скорее всего, получите 500. Однако спецификация GraphQL допускает как ответ данных запроса GraphQL, так и ошибку в ответе.
В функциональном программировании у вас нет исключений, но есть Result
или Either
, возвращаемые из функции. Хотя возвращать оба результата может быть бессмысленно, это возможно, если возвращать массив или кортеж, содержащий оба результата, подобно тому, как Go и Lua могут возвращать и данные, и ошибку в одном вызове функции.
Философия FP-мышления работает здесь, потому что мы НЕ хотим никогда не бросать исключения, а вместо этого возвращаем результаты, которые AppSync может понять. Это гораздо проще в FP-языках. Однако у ReScript есть оговорки, которые мы обсудим позже. Несмотря на это, Lambda, основанная на «лямбда-исчислении» — чистой функции с единственным входом и единственным выходом без побочных эффектов, — является для функциональных программистов как бы религиозным призывом к Serverless.
Это гарантирует, что все ваши внутренние вызовы находятся не в гигантской кодовой базе в стиле Express.js, а в небольшом наборе микросервисов разумного размера, которые могут быть в монорепо, но индивидуально развернуты и протестированы.
ReScript
ReScript — это, пожалуй, единственная часть технологического стека REGAL, которой я не вполне доволен. Я уже писал и говорил о том, почему я выбрал ReScript вместо других альтернатив. Однако он стоит первым в названии и занимает 2-е место по важности в техническом стеке, так что я приверженец.
ReScript — это язык и компилятор. Вы пишете на ReScript, и он компилируется в JavaScript, точно так же, как вы пишете на TypeScript, и он компилируется в JavaScript. Как и TypeScript, ReScript поддерживает интеграцию с существующим или новым кодом JavaScript во время выполнения. ReScript был выбран по следующим причинам:
- рядом с OCAML, это самый быстрый компилятор на планете; это соответствует философии Fast Iteration.
- Это разумно типизированное функциональное программирование; оно следует философии функционального программирования и корректности.
- Он компилируется в JavaScript, что позволяет нам использовать Lambdas на базе Node.js, которые, наряду с Python, являются одними из самых быстрых для выполнения (для функций с низкой задержкой, основанных на коротком времени; для пакетных или рабочих функций я бы предпочел Go/Rust/F#). Это также позволяет нам использовать множество библиотек JavaScript, включая AWS SDK для Node.js. Это очень помогает философии Serverless First.
Scala использует JVM, и хотя она поддерживается нативно, JVM слишком медленна и тяжеловесна для небольших функций. Компилятор Rust слишком медленный, и мы в основном выполняем вызовы ввода-вывода с простым разбором текста, поэтому не используем низкоуровневые возможности Rust. F# удивителен, и .NET поддерживается нативно, но опять же, .NET не так быстр, как Node.js, документация по F# как для языка, так и для AWS SDK ужасна, а AWS SDK для F# — это, насколько я могу судить, в основном привязка к C#. Haskell требует пользовательской среды выполнения (в первую очередь бессерверной, помните, никаких контейнеров). То же самое касается OCAML. TypeScript имеет много средств FP & библиотек, но его компилятор медленный, а типы более многословны при меньших гарантиях.
Elm
Elm — это язык, компилятор и менеджер пакетов для создания веб-приложений. Вы пишете на Elm, а он компилируется в JavaScript и HTML. Если бы я мог использовать Elm на внутренней стороне, я бы так и сделал, но я не могу, поэтому я использую ReScript.
Elm был выбран по следующим причинам:
- Быстрый компилятор. Я мог бы использовать TypeScript с библиотеками FP, но это медленно. Пока вы используете определения типов для всех ваших функций, Elm остается быстрым. Это соответствует философии Fast Iteration.
- Хорошо типизированный функциональный язык. Это соответствует философии функционального мышления.
- Отсутствие ошибок во время выполнения или исключений нулевого указателя. Это соответствует философии корректности.
- Компилятор настолько хорош, что позволяет проводить «бесстрашный рефакторинг». Хотя вы должны делать небольшие, инкрементальные изменения, вы, конечно, не обязаны это делать. Это хорошо сочетается с философией CICD, основанной на магистрали, поскольку многие разработчики могут вносить многочисленные изменения в кодовую базу, а компилятор прикроет вас.
- Библиотека elm-graphql позволяет генерировать код. Это позволяет нам распространить «бесстрашный рефакторинг» на весь стек. По мере того, как мы создаем, изучаем и в конечном итоге изменяем нашу доменную модель на основе этих знаний, мы можем регенерировать необходимый внешний код, а компилятор дает нам знать, что нужно изменить (я все еще не нашел ничего столь же мощного для ReScript).
- У Elm нет побочных эффектов. Это делает практику Test Driven Development намного проще, так как в модульных тестах не нужны Mocks/Spies, потому что все функции чистые. Вы все еще должны избегать использования String и использовать fuzz/property тесты, где это возможно.
Сплошная архитектура
Создание полнофункционального веб-приложения в стеке REGAL обычно состоит из 3 шагов, которые часто выполняются в различном порядке и многократно.
Моделирование домена
Вы моделируете свой домен на GraphQL, создавая запросы, мутации и типы в файле shcema.graphql. Это не является чем-то неизменным и будет меняться по мере вашего обучения.
Создание и развертывание BFF
Вы развертываете свою схему в AppSync с помощью Serverless Framework. Вы пишете свои Lambdas на ReScript для приема типизированных запросов с помощью Jzon и экспорта типизированных GraphQL-ответов с помощью Jzon. Вы связываете эти Lambdas с вашими GraphQL-запросами и/или мутациями через AppSync. Ваш пользовательский интерфейс выполняет HTTP GraphQL вызовы к URL CloudFront или Route53 вашего AppSync. Эти вызовы аутентифицируются токеном заголовка, JSON Web Token или другой лямбдой.
Сборка и развертывание пользовательского интерфейса
В своем пользовательском интерфейсе запустите elm-graphql для генерации всего кода GraphQL Elm, необходимого для взаимодействия с AppSync в типизированном виде. Вы делаете вызовы Elm GraphQL, которые возвращают данные, необходимые для заполнения вашей Elm-модели. Вы развертываете свой рабочий пользовательский интерфейс несколько раз в день, проверяя его на Github/Gitlab. Это запускает Amplify для начала сборки с помощью CodeDeploy. CodeDeploy имеет встроенный контейнер Cypress. Он запускает ваши тесты elm test, elm review и Cypress end to end и показывает результаты пройденных тестов и загружаемые видео. В случае успеха он будет развернут на S3 и кэширован в CloudFront. Вы можете получить доступ к этому пользовательскому интерфейсу через URL CloudFront или Route53.
Изменение схемы
GraphQL гарантирует, что типы в вашем пользовательском интерфейсе и API одинаковы. Это и хорошо, и плохо для компилируемых языков. В спецификации GraphQL говорится, что API GraphQL не имеют версий, и GraphQL должен быть обратно совместим. Этого безумно трудно добиться, и я не согласен с этой философией, но пошел на компромисс, потому что мне нравится, что философия правильности выигрывает больше, чем некоторые из грехов CICD, которые мы совершаем. Это означает, что пока вы добавляете поля, добавляете запросы и мутации, вы в порядке.
Как только вам нужно изменить имя или что-то еще, удалить поле или изменить поле… вы сломали API. Это означает, что сначала вам нужно изменить ваши лямбды, которые используют эти новые данные, и убедиться, что ваши модульные тесты (с помощью ReTest) прошли, а интеграционные тесты (с помощью Mocha & JavaScript), которые вызывают ваши лямбды непосредственно на QA-сервере, по-прежнему работают.
Это неизбежно потребует от вас исправления пользовательского интерфейса. Для этого нужно снова запустить elm-graphql с обновленным файлом schema.graphql, и компилятор Elm сообщит вам, что нужно обновить.
Этот процесс «изменение GraphQL, исправление тестов, генерация кода Elm, исправление ошибок компилятора» — обычная итерационная схема по мере создания и обучения. Это гораздо проще сделать в монорепо, но может потребоваться поэтапное развертывание, когда изменения вашей доменной модели успокоятся. Да, такое поэтапное развертывание НЕ соответствует философии CICD. Вы можете либо сделать GraphQL правильно с первого раза (удачи вам в этом), используя монорепо с помощью Amplify CLI, или Serverless приложения с несколькими развертываниями (подробнее об этом ниже), или просто пройти через это, зная, что компиляторы прикроют вас (исключение для интеграции & e2e UI тесты… они на JavaScript, и типы здесь вам не помогут). Мы рекомендуем монорепо для решения этих проблем (я пока не могу на своей работе, подробнее об этом ниже).
Развертывание
Развертывание пользовательского интерфейса — довольно простое дело. Напишите код, проверьте его, и установка Amplify CodeDeploy запустит ваши тесты, и если они пройдут, то развернет. Хотя Amplify может независимо развертывать в несколько окружений на основе ветвей, если мы следуем Trunk Based CICD, мы не можем этого сделать. Вместо этого мы создаем совершенно отдельный стек Amplify, один для QA, один для Stage и один для production на отдельной учетной записи AWS. На моей работе у нас есть QA, Stage и Production стеки, и все они берутся из одного репозитория Gitlab. QA и Stage запускаются и развертываются в одно и то же время. Production не имеет включенной автоматической сборки Amplify, и должен быть развернут вручную одним кликом в конвейере Gitlab (он просто делает вызов curl к Amplify, чтобы начать сборку). Amplify CLI обрабатывает все это по-другому и предполагает, что вы делаете функциональные ветки. Хотя вы получаете монорепо, мы нарушаем философию Trunk Based CICD.
Развертывание API может использовать любую систему сборки, которую вы хотите; CodeDeploy, Jenkins, что угодно. На работе мы используем конвейеры Gitlab. Они имеют ту же настройку, что и UI; lint, test, integration test. Разница, однако, в том, что сначала мы развертываем в QA, запускаем интеграционные тесты, и если они проходят, только тогда мы развертываем в Stage. Это гарантирует, что Stage остается стабильным для владельцев продуктов, которые хотят показать пользовательский интерфейс или протестировать его самостоятельно, не спрашивая разработчика: «Есть ли стабильная среда, которую я могу использовать?». Поскольку изменения UI редко что-то ломают, а API — да, мы используем более строгую настройку.
Откат
Хотя Amplify поощряет методологию fail forward, мы используем конвейеры Gitlab для отката. Все, что делает файл .gitlab-ci.yml, это «serverless deploy»; поскольку мы изменяем Amplify только в начале проекта, этот конвейер Gitlab на самом деле ничего не делает. Однако вы можете развернуть более старый конвейер, и мы используем это в случае, если развертывание сломает что-то в продакшене и нам понадобится легкий откат.
Для API — то же самое; каждое развертывание использует конвейер Gitlab на коммите, так что если один из них окажется плохим, мы сможем сделать откат. Однако мы поощряем встроенные зеленые/синие развертывания Lambda, чтобы вам никогда не пришлось делать откат.
Опции
Ниже перечислены различные опции, которые вы используете для изменения работы архитектуры REGAL.
Monorepo
Здесь у вас есть несколько вариантов. На работе мы используем 2 репо, одно для AppSync, развертываемого через конвейер Gitlab, и одно для Amplify, где Gitlab только sls развертывает, а CodeDeploy обрабатывает все остальное. Поэтапное развертывание происходит, если вы еще не знаете свою доменную модель, поэтому это побуждает вас выяснить ее как можно раньше. Монорепо снимет эту проблему и позволит вам развертывать проект на поздних стадиях, оставляя за собой право изменять GraphQL API.
Amplify CLI использует эту методологию и хранит ваш schema.graphql в одной кодовой базе, так что и ваш код UI, и код API имеют единый источник истины И могут обмениваться кодом, что значительно облегчает проблему размещения DRY в микросервисах. На моей работе мы не можем использовать Amplify CLI, потому что он требует создания ролей администратора IAM.
Serverless Framework и AWS SAM имеют возможность использовать вложенные приложения, но при этом развертывать их как единое целое в одном развертывании CloudFormation. Это позволит сосуществовать коду пользовательского интерфейса и коду API. Я отказался от этого в AWS SAM, потому что не смог заставить его хорошо работать с компилятором ReScript, в то время как в Serverless Framework «это просто работает» (sls deploy function
не работает, однако).
CSS Framework
Мы используем Tailwind на работе, потому что, поскольку 90% вашего времени в представлениях Elm — это создание HTML-тегов, классы Tailwind идут прямо в HTML, и это делает работу и проектирование в Elm очень удобным. Кроме того, компилятор Tailwind v3 работает на миллисекунды быстрее, что соответствует философии Fast Iteration для проектирования.
Elm не выносит суждений о том, как вы делаете CSS или какой фреймворк используете. Некоторые даже не используют CSS.
Route53 против CloudFlare
В начале проекта я просто использую URL CloudFront для UI и API, пока не буду готов интегрировать их. Я стараюсь интегрировать их как можно скорее, даже если просто использую Mocks, потому что считаю, что всестороннее тестирование очень важно. Как только это станет возможным, я настрою URL Route53, чтобы избежать CORS и облегчить обмен URL с партнерами по команде и продуктом. В конечном итоге, однако, мы переведем все на CloudFlare и проигнорируем Route53. Это связано с причинами, которые мне не хватит ума описать; суть в том, что CloudFlare отлично справляется с безопасной маршрутизацией трафика.
Только запросы AppSync
Почти в каждом учебнике по AppSync говорится о том, что «нужно запрашивать только те данные, которые вам нужны». Они показывают GraphQL-запрос, который возвращает JSON-объект с 11 полями, и они получают только 3 из них.
Однако, когда вы создаете BFF для пользовательского интерфейса, который вы также создаете… вы знаете, какие данные вам нужны. Именно вы создали фактическую схему GraphQL. Это вы определили типы, возвращающиеся из запросов и мутаций. Поэтому вы заметите, что ваш AppSync обычно просто связывает запросы и/или мутации с лямбда-функциями… и все. Вас не очень волнует «привязка поля FirstName типа Person к этой DynamoDB вот здесь…», потому что… это не то, что происходит.
Ваш пользовательский интерфейс делает вызов GraphQL «getPerson» и передает строку ID… а ваша Lambda получает этот вызов, обращается непосредственно к какому-то микросервису/базе данных, обрабатывает его, чтобы он соответствовал тому, как, согласно GraphQL, выглядит Person, и возвращает его. Нет необходимости во всем этом синтаксисе выбора. Да, в Elm вам все равно придется писать синтаксис выбора… но обычно вы либо автоматически добиваетесь успеха с помощью уже созданных типов, либо создаете свой собственный UI-тип и привязываетесь к нему.
Опять же, мы обращаемся с GraphQL, как с REST: делаем GET’ы или POS’ы. Единственная разница в том, что GraphQL гарантирует соответствие всех типов, и нам не нужно беспокоиться об ошибках преобразования. Все эти вещи типа «выбрать только подмножество полей» нужны, когда вы обращаетесь к API GraphQL, созданному другой командой, или это база данных с API Hasura, и вы получаете только то, что вам нужно. Это не то, что мы делаем здесь. Мы создаем back-end, который предоставляет именно те данные, которые нужны нашему пользовательскому интерфейсу.
Тем не менее, это AppSync. Вы можете настраивать его по своему усмотрению. Просто не удивляйтесь, когда ваши запросы дают вам данные, которые использует ваш пользовательский интерфейс, а люди говорят: «Чувак, почему ты всегда получаешь все данные?». Вы можете ответить: «Потому что это нужно моему пользовательскому интерфейсу». Я Full-Stack Dev, бееееееееее!!!».
Elm GraphQL Type Mapping
Библиотека elm-graphql проделала замечательную работу, сохранив синтаксис выбора GraphQL в безопасном для типов виде. Тем не менее, очень часто типы, которые вы запрашиваете, дублируются в Elm, чтобы вы могли изменить их позже для решения проблем пользовательского интерфейса, которых нет у бэкенда. Иногда они никогда не меняются, и это не дублирование и не нарушение YAGNI… это нормально.
Например, если у вас есть тип Person в вашем GraphQL, затем вы генерируете код в elm-graphql, вы, вероятно, получите пакет типа Api.Object.Person
. Однако в вашем пользовательском интерфейсе может потребоваться добавить к нему selected : Bool
, если он находится в списке, или какую-нибудь переменную состояния, например RemoteData, если вы обновляете конкретного человека. Таким образом, у вас будет свой собственный человек, определенный в Elm:
type alias Person = { id: String, firstName: String }
Затем вы напишете код отображения для него… и вдруг запутаетесь и спросите себя: «Почему я просто не использую сам Api.Object.Person
? Почему мне нужно выбирать данные из него? Вы можете сделать это, конечно, но это не очень хорошо документировано. Опять же, это предубеждение, что GraphQL используется разработчиками front-end для доступа к беспорядочным данным на back-end. Маркетинг не ориентирован на людей вроде нас, создающих собственные API. Если вы знакомы с Domain Driven Design, представьте, что Elm — это собственный ограниченный контекст. Да, у него есть четкий тип, определяющий, что такое Person на внутренней стороне, но он создает свой собственный на случай, если ему понадобится добавить данные, связанные с пользовательским интерфейсом, к своей версии Person. Затем она может быть отображена обратно на Api.Object.Person
, если вам понадобится сделать вызов мутации.
Сравнение других технических стеков
Ниже рассказывается о других популярных технологических стеках веб-приложений и о том, почему REGAL не использует их технологии.
ReScript React
ReScript довольно популярен среди пользователей React. Синтаксис ReScript, основанный на сопоставлении шаблонов, отлично работает при определении того, какое состояние нужно нарисовать в JSX вашего React’а. На мой взгляд, учитывая, что определения типов в ReScript в основном невидимы, он также гораздо более читабелен, чем многословные альтернативы TypeScript, такие как Relay.
Однако здесь есть две проблемы. Во-первых, React происходит от корней ООП. Их компоненты основаны на классах. Это не функциональность в первую очередь. Во-вторых, хуки React на самом деле не являются чистыми функциями, их очень сложно тестировать, они имеют дикие последствия (например, useEffect выполняется дважды) и приводят к большому количеству вложенных закрытий. ReScript не может исправить эти специфические для React проблемы.
Phoenix Framework
В Elixir нет типов. Elixir тоскует по временам Erlang, когда он был супербыстрым, живым развертыванием реального кода, в настоящее время на EC2 или, в крайнем случае, ECS/K8. Я не фанат штатов и серверов. В Elixir также есть исключения во время выполнения. Да, философия «пусть все рухнет» заложена в пользовательском интерфейсе, но… что если бы вам никогда не приходилось рушиться? Такая технология существует. Она называется Elm. Что если бы вы также могли развернуть такой пользовательский интерфейс и никогда не беспокоиться о том, что сервер упадет, «потому что Джефф Безос этим занимается?». (т.е. Amplify)
Svelte/Vue/Angular
Все они в значительной степени процедурные или объектно-ориентированные, имеют исключения во время выполнения, и их трудно тестировать. Кроме того, у всех есть усталость от того, какой маршрутизатор и решение/библиотеку для управления состоянием вы используете. В Elm вы «просто используете архитектуру Elm».
Будущие улучшения
Несколько вещей, как неизвестных, так и известных, которые появятся в будущем.
Улучшения монорепо
Было бы неплохо иметь рекомендуемый способ выполнения монорепо. Хотя наличие схемы GraphQL в двух репозиториях — не самая плохая вещь в мире, вы теряете время, если забываете скопировать-вставить при внесении изменений. Amplify CLI придется что-то придумать, если он хочет, чтобы те из нас, кто не может создавать IAM-роли с божественной силой, предоставили эти полномочия сторонним CLI. Serverless продвинулся здесь, используя мультисервисные развертывания. Мы должны иметь возможность изменить схему GraphQL с помощью ломающего изменения за несколько месяцев до начала проекта, и использовать супермощные, быстрые и потрясающие компиляторы Elm и ReScript, чтобы помочь нам… и не делать поэтапного развертывания.
Code Deploy Cypress Mystery Failures
CodeDeploy запустит ваши тесты Cypress, используя встроенный контейнер Docker, покажет, что все тесты прошли, а затем скажет, что они не прошли. Это блокирует развертывание. Нам приходится практически закомментировать фазу тестирования, чтобы довести код до QA/Stage/Production. Это продолжается уже несколько месяцев. Иногда это длится день, а на следующее утро волшебным образом исправляется.
Смена языка R с ReScript на Roc-Lang
Мне нужен Elm на сервере. Roc-lang обеспечивает это. Возможно, этой осенью я протестирую несколько пользовательских развертываний Lambda и посмотрю, что из этого выйдет.
ReScript GraphQL Generator
Возможность генерировать кодировщики/декодировщики Jzon с типами для GraphQL — это огромный плюс для производительности, которого нам не хватает на сервере. Для Elm невероятно, насколько тривиальными стали обновления с помощью code gen + compiler powers, в то время как с ReScript вам приходится вручную запускать интеграционные тесты на развернутой лямбде, чтобы проверить, работает она или нет. Большинство генераторов, которые я нашел, генерируют запросы, а не фактический код, декодеры, кодеры и типы, как это делает elm-graphql. Я потихоньку начинаю разбираться в том, как работают теневые типы в ReScript, чтобы попробовать это сделать, но мне немного не хочется этого делать, зная, что Roc-lang, возможно, будет лучшим вложением моего времени.
Выводы
Технологический стек REGAL позволил нам создать и развернуть 2 полнофункциональных веб-приложения менее чем за год. У нас было ноль сообщений об исключениях времени выполнения для пользовательского интерфейса в производстве, и все непреднамеренные исключения времени выполнения для ReScript Lambdas были изменениями данных микросервиса, которые Jzon имеет довольно хорошие исключения времени выполнения парсинга, гораздо более читаемые, чем Elm, проблемы с соединением, которые не были нашей виной, или интеграция JavaScript, которая была… ну, и моя вина, и вина JavaScript… потому что JavaScript.
Возможность быстрой итерации с уверенностью значительно повысила мою самооценку, уменьшила беспокойство по поводу изменений и позволила мне узнать больше о CICD и тестировании более безопасным способом, при этом продолжая приносить пользу бизнесу.
Компилятор ReScript позволил мне быстро вносить изменения в данные по всему нашему BFF по мере изменения модели данных микросервисов или улучшения моего понимания. Компилятор Elm позволяет всей команде вносить масштабные изменения в данные и функциональность, при этом мы уверены, что если что-то сломается, то «всегда виноват BFF». Amplify & AppSync упростили размещение веб-приложений на AWS, позволив нам сосредоточиться на коде, а не на инфраструктуре. GraphQL позволил нам уверенно моделировать, и по мере того, как мы учимся и меняем что-то, мы знаем, что компиляторы прикроют нас, чтобы мы могли вносить эти изменения, не опасаясь, что мы что-то непреднамеренно сломаем.
Философия REGAL — быстрые итерации, корректность, функциональное мышление, serverless first и trunk-based CICD — действительно сформировала то, как я создаю программное обеспечение сейчас, и сделала все более приятным в целом.
Помните, вам не обязательно принимать весь технологический стек или философию REGAL, чтобы что-то пошло вам на пользу; просто выберите что-то одно для игры и посмотрите, что из этого выйдет.