Подія відбулась
Подія відбулась

Квитки на наступну конференцію Конференція Node.js fwdays'23 вже у продажу!

AWS CDK. Infrastructure as a code with TypeScript [ukr]

Презентація доповіді

Розповім як ми проект без DevOpsʼів робили. Ми пройдемся по основних поняттях AWS CDK. Познайомимся з сервісами, напишем та задеплоїм на AWS аплікацію для юзер менеджменту.

Юрій Мирош
Lemberg Solutions
  • В веб розробці більше 10 років, 9 з яких в компанії Lemberg Solutions, в якій пройшов шлях від Middle до Head of JavaScript Department.
  • Починав свій шлях як PHP Developer, з виходу Angular 2 почав цікавитися JavaScript, та згодом не помітив як став тім лідом JavaScript команди.
  • Мав досвід з Angular, Node.js, NestJS, React.
  • Twitter, Facebook, LinkedIn

Транскрипція доповіді

Всім привіт. Ради вітати вас на конференції FW Days з темою доповіді "AWS CDK: Infrastructure as Code with TypeScript". Розпочнав з пару слів про мене так, як мене представили мені в IT 11 років. Розпочнав я з PHP. На даний момент моя посада називається хедом в JavaScript Department в Lamberth Solutions. Lamberth Solutions – це компанія, яка зараз налічує більше 200 людей. Відділи в нас поділяються не по проектах, як у переважній більшості компаній, а по технологіях. Відповідно, JavaScript відділ – це відділ, яким я займаюся. Зараз налічує близько 20 інженерів. Основний наш фокус – це Angular, Vue, React та Node. Якщо говорити про мій власний досвід, то так, я починав з PHP, потім сплавно перейшов в Node і Angular. На даний момент найбільшими, найсильнішими своїми технологіями, я вважаю, це Node, Angular, React і AWS. Отже, переходимо до теми нашої доповіді. Як завжди, треба починати, напевно, з запису проблеми, яку ми старалися вирішити. Тему доповіді я взяв з проекту, з яким до нас прийшов клієнт, на якому ми допомагали йому вирішувати його проблему.

Отже, яка була проблема – це клієнт до нас звернувся з запитом попробувати розбити великий моноліт, але не як всі на маленькі мікросервіси, а на self-contained systems. Тобто, ідея була така – взяти великий моноліт, який вже давним-давно працює клієнт, і розбивати його на окремі частини, якими могли би займатися окремі тіми, і він не був би так зав'язаний один на одному. Відповідно, звучало дуже круто взяти одну з більш-менш виділених підсистем і спробувати зробити з неї self-contained system. Але проблема була в цьому, що в DevOps команди не було капаситі нас сапортити. В цей момент вигляд я мав приблизно такий. І другі необхідні дані, які прийшли до нас, це все треба зробити за три місяці, оскільки клієнт готовий тратити три місяці на валідацію своєї ідеї, і якщо воно далі піде, то далі її використовувати, якщо ні – відмовитися від неї, оскільки старий моноліт в принципі працює, для чого його чіпати з точки зору бізнесу. В цей момент, коли ми почулися про три місяці, приблизно вигляд в мене був ось такий.

Ми почали думати, шукати, як це зробити без DevOps, без часу на якісь роздуми. І подумали про таку штуку, як AWS CDK. AWS CDK – це Cloud Development Kit, це продукт від AWS, який дозволяє нам описувати інфраструктуру кодом, і більше того, дозволяє нам описувати це нашою любимою мовою програмування. В даному випадку це ми писали на TypeScript. Самі основні концепти, які є в основі CDK, CDK складається з аплікації, аплікація складається зі стеків, стеки складаються з конструкцій, на аплікацію можна вмістити декілька стеків, декілька стеків можуть вмістити декілька конструктів. В кінці кінців це все описується кодом, описується в об'єктно-орієнтованому стилі, і з цього все генерується, звичайно, CloudFormation Template. Хто не знайомий з AWS, CloudFormation Template – це темплейт, який дозволяє нам розгорітати AWS сервіси, автоматизувати їхній запуск і конфігурацію. І він генерується в результаті виконання цього AWS CDK, і в кінці ми маємо готові AWS ресурси, з якими ми можемо працювати. В коді виглядає воно приблизно таким чином. Ми маємо стек, оце, що зараз виділене, ви бачите на екрані, це, в принципі, стек. Стек складається з декілька конструктів, оце, що виділене в даному випадку, це створення DynamoDB таблиці, воно виглядає ось так. Ми звертаємося до DynamoDB таблиці як до об'єкту, кажемо, створюємо new table, передаємо якісь параметри, проперті, і таким чином вказуємо, декларуємо, що ми хочемо створити DynamoDB таблицю. Які використання цього підходу, як на мене?

Як на мене, першим плюсом великим, яким для нас був, у нас була невелика команда з трьох людей. Це те, що ми можемо використовувати це, що ми вже знаємо, нам не потрібно щось окремо вчити, ми можемо писати все на TypeScript. Другим великим плюсом – це саме створення сервісів. Нам не потрібно ходити в консольку їх створювати, або не потрібно писати CloudFormationConfig, який, на мою думку, доволі важкий для розуміння і треба розуміти, як його писати, а ми можемо писати об'єкти. Ми можемо деплоїти нашу інфраструктуру одночасно з кодом. Тут ви можете бачити, як це легко робиться. Ми маємо код, маємо CDK. CDK має в собі ще одну перевагу, я вважаю, – наявність User-Friendly CLI, за допомогою якого ми можемо швидко генерувати код. Ми можемо швидко деплоїти і розгортати нашу інфраструктуру в AWS. Також перевагою є велика кількість вже побудованих конструктів, які ми можемо брати і використовувати. В принципі, під кожен AWS сервіс AWS надає конструкт, який ми можемо взяти і використовувати. Плюс є багато ще конструктів, які написані в ком'юніті, і так само ми можемо використовувати. І як це все буде виглядати?

Я підготував коротеньку демку, дуже простий фронтенд. В принципі, це крут, Create, Read, Update, Delete юзерів. Я сильно над фронтендом не задумувався, тому що більша ідея, більший фокус показати іменно на роботу з CDK. Дуже проста табличка, яка допомагає створити, видалити і редагувати юзерів. Приблизно таке, що ми будемо створювати. Архітектура цього дуже проста. Ми маємо React-аплікацію, яка буде через GraphQL AppSync. Це сервіс AWS, який провадить нам GraphQL інтерфейс для комунікації в даному випадку з DynamoDB таблицею UserStable через лямди.

Ми створимо набір лямд, який створює, редагує, видаляє і апдейтить юзера. І це все пов'яжемо з GraphQL AppSync. Для цього ми будемо використовувати наступні речі. Це CloudFormation, в який буде білдатись все, що ми напишемо. В нас буде BackendStack один, який буде складатися з трьох конструктів з DynamoDB таблиці, з AppSync і з лямди. Це вже код. Давайте перейдемо. Я продемонструю цей фронтенд. Виглядає він в принципі так. Зараз він працює на моках. В кінцевому результаті ми зробимо так, що він буде працювати з реальним AWS за допомогою GraphQL. Буде спілкуватися з ним. Але давайте почнемо з того, як взагалі, з чого почати. Почнемо ми з того, що згенеруємо пустий темплейт. Я буду використовувати NPX. Як я вже говорив, в нас є дуже зручна CLI. MyApproperation.xcdk.init. Треба вказати мову. І зараз для нас розвернеться пустий шаблон CDK-аплікейшена. Так, все вже створилося. Я відкрию це в S-коді. Так, що ми зараз бачимо? Для нас згенерилася аплікуха, яка складається з FWI-стека.

І давайте попробуємо написати якийсь конструкт. У нас навіть тут є закоментований код, для того, щоб щось спробувати і з цим побавитися. В даному прикладі використовується SQS-сервіс. Це сервіс черг від AWS. Ми можемо просто розкоментувати цей код. Зберегти. І попробувати його розвернути в ауді. Що нам для того потрібно? Спочатку нам потрібно це все збілдити. Десь я зробив якусь помилку. А, він вже збілдений. Значить нам потрібно запустити NPX CDK Bootstrap, який створить все, що нам необхідно на AWS-консолі. Так, ми бачимо, що зараз ніяких змін не було. І ми можемо запустити CDK Deploy для того, щоб розгорнути це все на AWS-аккаунті. Так, ми бачимо, зараз створюється CloudFormation-темплейт.

І бачимо, що стек починає створюватися. Щоб виприконати, що він дійсно створюється, я пропоную зайти в AWS-консоль. Ми бажаємо відкрити CloudFormation-сервіс. Якщо його вновити, ми бачимо, що наш стек почав створюватися. Трошки зачекаємо. Поки чекаємо, в принципі, можемо оглянути, що в нас в коді. Відповідно, нам створився стек, який створює Q. Саме для нас створилося це в папочці lib. Папочка cdk-out – це за допомогою команди cdk-bootstrap ми генеруємо це, що потрібно для розгортання.

І, увага ваша, я би хотів звернути на cdk-json. Це файлик, який вказує, що цей проект збудований на CDK. Він виглядає таким чином. Як ми бачимо, консоль нам повідомила, що все створено. Можемо йти в SDK дивитися. Ми бачимо, що стек create-complete. Якщо ми його відкриємо, ми можемо глянути, які ресурси було створено в цьому стеку. І ми побачимо, що створено ресурс, який називається fwdsq. І також CDK створює свій ресурс, який називається cdk-metadata. Перевіримо, чи дійсно Q було створено. Для цього перейдемо в SQS консольку. І так, ми бачимо, що Q було створено. І так само ми можемо це все почистити за собою, за допомогою команди cdk-destroy. Так, нас перепитається, чи ми дійсно впевнені, що ми хочемо видалити весь стек.

Ми нажмемо, що ми хочемо. І ми можемо побачити, що наш стек видаляється. І через якийсь час досить швидко він задестроїться і почистить за собою всі сервіси, які були створені. Отже, таким чином ми можемо досить просто маніпулювати з сервісами. На мою думку, набагато зручніше, ніж це робити через CloudFormation. На мою думку, звісно, набагато зручніше, ніж це робити прямо з консолі, оскільки прямо з консолі це взагалі не є добрим продакшн-рішенням. Відповідно, знаючи це, ми приступили до роботи. І, в принципі, з цим проєктом, який я розказував на початку, за три місяці ми впоралися. Але давайте продемонструю я вам... Так, зараз ми бачимо, що воно видалилося у новому консоль. Так, так, видалився. Продемонструю вам, як працює аплікейшн, про який я говорив, зі створенням юзерів. Для цього так само перейдемо в код. Для економії часу весь код я написав наперед.

Відповідно, я буду про ньому проходитися і коментувати, не буду писати його зараз заново. Ми маємо, перш за все, створити DynamoDB таблицю. Для цього структура файлів в мене наступна. Ми маємо файл зі стеком і маємо папочку з конструктами, де лежать наші AppSync, AppSync Lambda, ConstructBuilder – це інтерфейс, який я створив для того, щоб всі наші конструкти мали метод build і ми могли їх легко викликати однаково в стеку. Почнемо з бази даних. Для даного прикладу я вибрав DynamoDB, але ви можете вибрати будь-яку іншу базу даних, яку вам потрібно. Можете використовуватися всім, що є в арсеналі AWS. Ми створюємо table. Ви бачите, що ми з пакету AWS CDK lib беремо AWS DynamoDB конструкт.

В нього є вже написаний для нас клас table. New table. Першим параметром ці конструкти приймають стек, до якого воно відноситься. Другим – ідентифікатор або назву таблиці. Я назвав це users table. І так само ми бачимо, маємо вказати ID для таблички. І на що би я дуже хотів звернути вашу увагу – це removal policy. Отже, в AWS стекі бувають двох типів. Є persistent і звичайні конструкти. Конструкти бувають двох типів – persistent і звичайні. Persistent – це в більшості випадків належить до сторіжів і до баз даних. І по дефолту вони за собою не чистяться, коли ми деструюємо стек. Це зроблено для того, що коли ми працюємо з даними, і ми задеструємо наш стек, ми не хочемо, щоб наші дані вже щезли.

І тому ми маємо це вказати вручну, що ми хочемо привиделені стеку, щоб так само чистилася наша табличка. І видалялися всі ресурси, які пов'язані з DynamoDB. Це дуже важливо, коли ви щось девелопуєте, або ви бавитеся для цього, щоб з тим познайомитися, оскільки, якщо не почиститься табличка, через якийсь час AWS почне за вас чаржити гроші за цю табличку, але ви нею, в принципі, не користуєтесь. Тому важливо не забувати про цей removal policy. Отже, ми маємо базу даних, створену табличку. Так само ми маємо після цього створений Upsync сервіс. Upsync сервіс створюється абсолютно так само. Ми маємо GraphQL API, яке ми беремо з CDK Lib.

Першим параметром так само передається стек, другим параметром передається ID, і далі йде набір пропертій, який конкретно відноситься до цього конструкту. Отже, ми можемо дати назву, вказати, де лежить схема. Тут дуже зручно, оскільки frontend і backend я організував в Monorepo, і, в принципі, frontend використовує ту саму схему GraphQL, яку використовує backend, і нам не потрібно думати, як їх шарити. Ми можемо звідкись брати, просто вказати шлях до неї. X-Ray Enabled – це для дебагінгу, і авторизація. В даному випадку ми взяли авторизацію, APK для того, щоб в демо просто прописати API-ключ, і frontend буде з backend спілкуватись за допомогою нього. В продакшені, можливо, вам потрібно буде щось інше.

Так само я хочу звернути вашу увагу на такі речі, як CFN-аутпут. CDK подумав за нас і придумав таку штуку, як аутпут, в яку ми можемо щось віддавати. І для того, щоб працювати з frontend, з нашим backend, нам потрібно endpoint GraphQL, на який ми будемо звертатися, і також нам потрібно знати цей API-ключ, який ми будемо використовувати для конекту до GraphQL. Відповідно, ми їх прописуємо тут в CFN-аутпуті. І далі, я покажу чуть пізніше, наглядно, але коли ми будемо деплоїти наші зміни, цей endpoint може мінятися, APK так само буде генеруватися для нас, і ці дані будуть записуватися в JSON-файл, який ми будемо використовувати на frontend як конфіг. Друга річ, яка потрібна нам, крім апсинка і таблиці, це самі лямби, які будуть опрацьовувати записи. Так само вони створюються… В принципі, я не буду сильно на них зупинятися. Так само вони мають свої проперті, які вже відносяться до самих лямб.

Тобто нам потрібно вказати runtime, нам потрібно вказати handler, нам потрібно вказати, де лежить код. В даному випадку він лежить в папочці lambda.graphql. Ось тут в мене є папочка lambda, папка graphql, і тут лежать вже самі лямби, які відповідають за створення. Так, наприклад, виглядає створення юзера в DynamoDB таблиці. Для цього я вже використовую AWS SDK, щоб маніпулювати вже з даними, які є в таблиці. Так, і що ще важливо, нам потрібно вказати data source для нашої graphql. Ці лямби, які ми прописали, ми маємо вказати як data source для graphql. Це робиться на 26-й строчці. І так само ми в data source створюємо резолвери під graphql-query і під mutations. Нам потрібно витягнути юзера по ID, нам потрібно витягнути всіх юзерів, нам потрібно створити юзера, апдейтити юзера і так само видалити юзера. Це все, що відноситься до лямб.

І останнім кроком є надання аксесу. Тобто так само за допомогою AWS SDK ми можемо не тільки створювати сервіси, а ми можемо давати доступ одному сервісу до іншого сервісу. Для демо я використав grant.fullaccess метод, який є в табличці. Насправді в реальному проєкті я б рекомендував не використовувати fullaccess, а давати стільки перемішанів, скільки потрібно лише для створення і для считування даних з табличок. Окей, давайте спробуємо це розвернути. До цього ми запускаємо нашу вже відому команду cdk-bootstrap і deploy. Як ви бачите, cdk подивився, які зміни потрібно були зааплоїти, і запитався, чи ми дійсно хочемо деплоїти це все. Можна сказати yes, і процес пішов. Так само ми можемо це побачити в CloudFormation стеку. Наш стек зараз review in progress, create in progress.

Поки він створюється, ми можемо зайти, напевно, щоб подивитися ресурси. Для нас створилася юзер-табличка. Ми бачимо, що вона вже є створена. GraphQL зараз in progress і AppSync Lambda in progress. Оскільки створилася вже табличка, щоб не тратити час пізніше, давайте перейдемо в DynamoDB. Ми бачимо tables. Users table вже створений. Можемо його відкрити. На даний момент зараз нема ніяких айтемів тут. Як справи з нашим стеком? Create complete. Видно, бачимо, що все створилося. Пропоную перейти в AppSync. І продемонструю, як цим користуватися можна насправді з консольки. Ми бачимо назву нашого AWS AppSync. Бачимо endpoint, який створився. Бачимо AppID. І бачимо APK. Єдине, що я чуть-чуть забув, коли деплою це все. Одну важливу річ. Як я говорив про front-end, що нам потрібно буде ці аутпути на фронт-енді.

Для того, щоб ними користуватися, нам потрібно, коли ми деплоємо, вказати наступне. Куди покласти аутпут? Для цього ми можемо зробити... Ми бачимо наші аутпути тут, зараз в консолі, але з консолі їх переносити незручно, тому ми можемо це автоматизувати і зробити таким чином. Outputs files. Outputs file. І шлях, куди ми хочемо покласти наш аутпут. В даному випадку я хочу його покласти в папку front-end. В папку SRC. APK, Export. Зроблено. Так, поки в нас буде перебіл дежувати, і мало би покласти нам папку. Зараз глянемо, чи дійсно так відбулося. Для цього зайдемо в папочку front-end. В SRC, CDK, Export, JSON. Ми бачимо файлик, який має Absinthe-K і Endpoint. Чудово.

Тепер повернемося до AWS консолі. Якщо ми перейдемо в GraphQL, ми можемо подивитися якийсь query. Пропоную створити перший mutation і створити юзера. Для цього вкажемо якийсь email, ID. DynamoDB не вміє сам генерувати ID. На фронті ми будемо використовувати ID для цього. Тут зараз я пропишу якусь ID сам. І test name, наприклад. Тепер ми можемо це все виконати. Бачите, що воно виконалося. І пересвідчити, що воно виконалося, перейшовши в DynamoDB табличку. І так, наш юзер створився. Бачимо, що він є. Так само ми можемо, наприклад, викликати query, юзер list, наприклад, нам потрібно email і name. Так, ми бачимо, що юзер для нас повертається. З точки зору AWS все працює. Так само ми можемо пересвідчитися, що наші лямди були задеплоєні. Тут ми бачимо лямди. Можемо подивитися їхній код. Так, всі функції, які були написані для створення, видалення юзерів, вони є тут.

Окей. Останній крок, що нам лишилося, нам лишилося заставити наш front-end працювати не з mock-ами, а з настоящим upsync endpoint. Для цього перейдемо в код. Перейдемо в front-end. Front-end – це React-апка, як я говорив. Для економії часу я наперед заготовив в кожному з компонентів такі речі, як кастоміні хуки для витягування, наприклад, для витягування юзерів. Він виглядає таким чином, ми використовуємо Apollo Client, як я казав, для того, щоб витягнути список юзерів. І я прописав, якщо mock, то ми тянемо дані з mock, а якщо ні, то ми вертаємо... Верніші, якщо не mock, ми тянемо дані з Apollo Client, а якщо це mock, то ми тянемо їх з mock.

Відповідно, ми можемо просто перейти в user table, позабирати сюди mock. Навіть спочатку заберемо в get users і глянути, чи все працює. Так, ми зараз побачили цього юзера, який був в нас на DynamoDB. Давайте заберемо mock з видалення юзера. Так само нам потрібно розкоментувати код, для того, щоб коли ми видалимо юзера, працював Apollo Cache. Верніші, я чечу, все, Apollo Cache. Так, попробуємо видалити юзера. Бачимо, що юзер видалений. Саме ми можемо зробити з Edit, Create. Так, виходить все. Попробуємо створити юзера через консоль. А я через веб, верніші.

Бачимо, що запит прийшов, і юзер створився. Перевіримо, чи він створився в DynamoDB табличці. Так, він створився в DynamoDB табличці. Тим чином ми пересвідчили, що все працює. І так само останнім кроком, після цього, як ми все протестили, я пропоную почистити за собою все. Наразі DK нас перепитає, чи ми дійсно хочемо видалити. Так, ми дійсно хочемо видалити наш стек і всі ресурси, які були пов'язані з ним. І ми бачимо, що процес видалення почався. В принципі, можемо перейти до заключної частини слайдів і підбити якісь підсумки.

Отже, якщо говорити про підсумки з реального проекту, на якому я працював, то він був трошки складніший і використовував трошки більше сервісів. Ми використовували, якщо говорити про сервіси, ми використовували DynamoDB таблиці, ми використовували SQS. Ми мали три різних черги, які ми мусили між собою якось об'єднувати. Тобто, з старим монолітом ми працювали через SQS-черги і, взагалі, старалися вибудувати це все так, щоб максимально не трогати старий моноліт. В принципі, у нас і вийшло, ми взагалі його не чіпали, він працював, як працював. І ми до нього під'єднувалися за допомогою...

Ми під'єднувалися до черг, які використовував він, і будували цю нашу нову систему. І я можу сказати, що систему ми збудували. Проблемою, з якими ми стикнулися, тут, в принципі, я виписав їх в мінуси, це перша проблема, яка була, це кожен девелопер мусив працювати окремо. Тобто, під кожного девелопера розтворювався AWS Developer Account. Я трошки, насправді, обманув вас, коли сказав, що це не було взагалі DevOps-ів задіяно. DevOps-и були задіяні для того, щоби постворювати ці аккаунти і менеджити аксес на них, щоби ми не постворювали щось забагато і там не вижирали бюджети.

Але вже всередині цієї пісочниці, яка нам була виділена, ми могли бавитися, як хотіли, в принципі, що ми і робили. Немає ніякого нормального емулятора для того, щоб бавитися з цим локально. Емулятор, який є, він нам не підходив. Він деколи на емуляторі все працює, на реальному AWS-аккаунті не працювало. Відповідно, ми від цього відмовилися і ми працювали, напряму, з реальними AWS-аккаунтами. І проблема, яка також в нас була, це тести. Одно з вимог було, що ми маємо покрити хоч якось НТН-тестами, там не говорилося ні про яке повне покриття за три місяці. Але хоч щось, основні речі ми мусили покрити.

І, мінімум, для себе ми мусили впевнитися, що дані, які приходять до нас, не ламають нічого на моноліті і не ламають нічого на цій новій системі. Відповідно, НТН-тести, в кінці кінців ми їх все-таки написали, використовуючи ще один AWS-сервіс, називається Step Functions, який, в принципі, як працював? Він запускався Step-функцією. Першим її кроком було підняти це все середовище, другим кроком прогнати тести, третім кроком, якщо тести окей, якщо раніше був фейл, відправити через SNS-пуш, що щось не так, і почистити все за собою. Це, насправді, не дуже була тривіальна задача, не дуже типова.

Готових рішень ми не знайшли, відповідно, ми це писали самі. Але рішення для цього є. А які плюси? Плюси, найбільшим плюсом, я вважаю, це те, що ми пишемо інфраструктуру кодом. Особисто мені, я більше працював з АОП, для мене взагалі не було проблеми переключитися і описувати нашу інфраструктуру об'єктами. Друге – це те, що ми можемо деплоїти одночасно і код, і сервіс. Тобто, якщо нам потрібен якийсь новий сервіс, ми просто його створюємо, і просто за допомогою CDK Deploy він опиняється в нас в AWS-аккаунті. Дуже все просто. Ми не тратимо операційний час на те, щоб прокомунікувати з DevOps, що нам потрібен такий сервіс, який нам потрібен і так далі.

І так само ми можемо дуже легко і дуже гнучко меншити наші перемішани. Я маю на увагу перемішани для сервісів, які надаються одним сервісам для доступу до інших. Так само ми все робимо з коду. Останній пункт, який я говорив, що можливо написати аплікацію без DevOps, але насправді не дуже правда, тому що DevOps нам потрібні були для того, щоб насетапити нам аккаунти і перемішани, хоча ми могли це зробити насправді самі, але це більше про зони відповідальності.

І так само девелопери. У нас було три девелопери, які це писали, і всі вони були, в принципі, були дотичані до AWS, всі мали якийсь попередній досвід з AWS і розуміли, які сервіси нам потрібні, знали, якими сервіси будемо використовувати, як їх конфігурувати. Відповідно, це було все так. Останнім – це набір лінків, які я підготував. Лінки про AWS CDK, конструкц-дев – це набір конструктів, які ви можете брати готові і використовувати. Ця демока залита на мій GitHub, також тут є в лінках. І мій LinkedIn, мій Facebook. Дякую вам всім за увагу. Можемо переходити до запитань.

Увійти
Або поштою
Увійти
Або поштою
Реєстрація через e-mail
Реєстрація через e-mail
Забули пароль?