Как стать автором
Обновить
45.53
Рейтинг

Разработчик в стране Serverless: первые шаги, первая лямбда (Часть 1)

Блог компании Lineate JavaScript *Программирование *Node.JS *Serverless *

Предисловие

Ссылки на другие части:

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

  • гибкое масштабирование;

  • быстрота разработки;

  • уменьшение времени или затрат на администрирование приложений;

  • быстрые релизы.

Преимущества выглядят заманчивыми и многообещающими. Так ли все это? Пришло время познакомиться с serverless технологиями. Буду разбираться с новым подходом через призму опыта создания “классических” приложений. Это значит, что обязательно должны быть тесты, возможность запускать локально, возможность дебажить, несколько окружений, логи, метрики и т.д. Нет смысла знакомиться на уровне hello world приложений, которые красиво выглядят на экране. Я возьму задачу с похожими на реальность сценариями. Конечно, в сети очень много разных статей и инструкций, но нет таких, после которых ты сможешь сделать как надо, подумав обо всех проблемах заранее, а не после релиза. Поехали.

Начнем с задачи. Буду делать REST API для небольшого баг трекера. У нас есть проекты, есть люди, которые работают на этих проектах. Есть карточки - таски, которые назначаются на людей. У карточек, само собой, есть статус выполнения. У каждого проекта есть свой репозиторий на гитхабе. Выглядит это примерно так:

Я буду реализовывать следующие методы:

  1. получить проект по идентификатору с информацией о людях, которые на нем работают, а также информацией об открытых PR;

  2. обновить информацию о проекте;

  3. добавить человека на проект;

  4. убрать человека с проекта.

Конечно, задача не боевая, рисковать нервами и чутким сном не хочу, но важно, чтобы она была приближенной к реальности. Я не собираюсь учиться кодировать, не буду весь код показывать, я хочу попробовать serverless подход и буду концентрироваться на этом. А поэтому неважно, какой код у меня используется для выполнения запроса к БД, важно, где и с какими параметрами этот код вызывается . Практически в любом проекте есть хранилище. Я буду использовать MySQL. У меня будет своя интеграция с 3rd party service через REST API в методе про получение информации о проекте.

Какого провайдера выбрать? В последнее время их становится все больше, даже у оракла появляется свой serverless. Начну с флагмана, с Amazon. Хочется верить, что он там максимально стабильный, понятный, обкатанный, да и искать ответы на вопросы будет быстрее и проще.

План будет примерно такой:

  1. Разбираемся с лямбда функциями, делаем первую простую лямбда функцию.

  2. Разворачиваем БД.

  3. Интегрируем лямбда функцию с БД.

  4. Создаем REST API и подключаем ранее созданные функции.

  5. Выходим в “прод”, нюансы о которых не надо забывать.

Глава 1. Первые шаги. Первая лямбда

Для начала мне надо подготовить рабочее место:

  • любимая среда разработки (я буду пользоваться VSCode);

  • система контейнеризации Docker;

  • AWS аккаунт с полными правами (в дальнейшем затронем этот вопрос детальнее).

С чего начать? Начнем с кубиков, из которых и состоит классическое serverless приложение - у Amazon это сервис AWS Lambda. Лямбда-функции - это функция, которая обрабатывает события из разных источников, начиная от очередей сообщений и заканчивая событиями от S3 об обновлении/добавлении файла. Лямбда принимает на вход json объект, представляющий это событие, делает с ним, что требуется, и возвращает ответ. 

Выбираем язык программирования

И вот тут у нас возникает первый серьезный вопрос: на каком языке писать? Казалось бы, можно писать на чем угодно, но все не так просто. И для того, чтобы понять, в чем проблема, придется немного погрузиться в документацию и рассмотреть вопрос, как лямбда работает. 

Жизненный цикл лямбды состоит из трех фаз:

  1. Init

  2. Invoke

  3. Shutdown

Как следует из названия, в фазе Init происходит создание окружения, среды выполнения, где код лямбды будет работать, происходит скачивание кода функции, запускаются конструкторы, код инициализации. Эта фаза “поднимает” лямбду в облаке. Происходит это один раз за все время жизни экземпляра лямбда функции. 

После фазы init наступает фаза Invoke. В фазе Invoke происходит непосредственный вызов кода обработчика лямбды функции. В отличие от фазы Init, которая происходит один раз, эта фаза может повторяться много раз; зависит от того, сколько событий будет доступно для обработки. Как только обработчик вернул результат, Amazon “замораживает” окружение, пока не появится новое событие для обработки. При появлении нового события, Amazon “размораживает” окружение. Если в процессе работы фазы происходит критическая ошибка или время выполнения выходит за пределы, то Amazon пытается пересоздать среду выполнения.

Ну и фаза Shutdown “убивает” лямбду и освобождает все ее ресурсы. Происходит это по прошествии какого-то времени, когда никакие события не приходили, и моя функция ничем не занималась. В документации не описано “время простоя”, но оно есть. 

Ну и при чем тут язык программирования? А при том, что он напрямую влияет на фазу Init. У нас есть лямбда, нет никаких созданных экземпляров этой лямбды, не приходят события. Прилетает первое событие. В облаке некому обработать это событие (также это бывает, когда все экземпляры заняты обработкой других событий). Провайдер начинает создавать для моего события экземпляр лямбды. “Срабатывает” фаза init. В этот момент я просто жду, когда появится рабочее окружение для лямбды. После того, как фаза отработала, на следующей фазе Invoke лямбда делает полезную (для клиента) работу. Эта ситуация называется cold start — когда нет доступных для обработки экземпляров функций и тратится время на поднятие этих функций (примерно как мы тратим время на прогрев двигателя автомобиля зимой в -30). Чем быстрее пройдет фаза Init, тем быстрее начнется обработка события, тем быстрее клиент получит ответ, тем меньше будет время ответа. В силу своей природы cold start у разных языков программирования разный. У интерпретируемых языков (python, nodejs) он существенно меньше, чем у компилируемых (java, c#). Но у компилируемых есть преимущество в лучшем использовании ресурсов, у таких лямбд “КПД” выше, если надо использовать несколько ядер, больше памяти. Поэтому, если нужно минимальное время cold start, берем что-то из интерпретируемых языков; если нужно по максимуму выжимать ресурсы из окружения, то лучше взять компилируемые.

Я провел небольшой тест, создал лямбды с hello world обработчиком на разных языках программирования. Ниже представлены длительности init фазы в зависимости от языка:

Основываясь на этих данных и на своих знаниях ЯПов, я остановил свой выбор на NodeJS.

Структура проекта

Начну с лямбды для получения информации о проекте. Код в принципе можно писать прямо в редакторе в Amazon Console. Достаточно создать hello world лямбду, дальше можно редактировать код этой лямбда функции. Очевидно, что этот вариант в любом не hello-world проекте обречен, не получится построить нормальный CI/CD, да и вообще это будет неудобно. Можно создавать лямбду с нуля, а можно взять за основу готовый шаблон. Поможет мне в этом SAM framework, расширение AWS Cloudformation  (далее CF) для Serverless-приложений. Он может создать мне hello-world шаблон для лямбда функции, поможет с деплоем в облако и не только. 

Я воспользуюсь hello-world шаблоном. Следующая консольная команда создаст hello world проект с лямбда функцией с использованием Node JS, а созданная лямбда функция при сборке будет упаковываться в zip архив.

serverless-bugtracker> sam init --package-type Zip --runtime nodejs14.x --app-template hello-world --name serverless-bugtracker

Структура первой лямбда функции получается такой:

template.yaml — это конфиг SAM framework, описывающий компоненты, которые задействованы в моем проекте. SAM шаблон совместим с CF шаблоном. Он добавляет способы быстро и просто создать специфичные для serverless приложений типы ресурсов (лямбды, api gateway, и т.д.). CF развернет из этого конфига ресурсы, которые в нем указаны. Если в template.yaml будет описано 10 лямбда-функций, RDS, SQS, то он это все создаст. Но здесь надо соблюдать грани разумного, ведь даже в serverless мире можно создать serverless-монолит, когда десятки разных ресурсов, которые могут быть не связаны между собой, находятся в одном конфиге. Такой подход делает поддержку приложения сложнее.

Приложение с лямбда функциями очень похоже на приложение с микросервисной архитектурой. Каждую лямбду можно рассматривать как отдельный микросервис. Поэтому для максимальной изоляции и независимости я буду размещать код функций в различных папках, со своими отдельными независимыми package.json. Оставлю корневой package.json для запуска тестов.

Финальная структура проекта:

Сделаю еще одну остановку и чуть подробнее посмотрю на template.yaml. Из каких частей он состоит, для чего они нужны? 

SAM шаблон

Начну с самой неинтересной части, которая содержит информацию о версии формата шаблон CF и о том, что это SAM шаблон: 

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31

Есть отдельный блок для текстового описания и глобальных настроек:

Description: >
  serverless-bugtracker

  My bug tracker.Uses Amazon serverless stack
 
Globals:
  Function:
    Timeout: 3

Один из самых важных блоков — блок описания ресурсов, в нем описываются все ресурсы для развертывания: лямбды, базы данных и прочие сервисы. Например, ниже приведен пример описания лямбда функции:

Resources:
  GetProjectByIdFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: src/handlers/get-project-by-id
      Handler: app.lambdaHandler
      Runtime: nodejs14.x
      Events:
        HelloWorld:
          Type: Api
          Properties:
            Path: /hello
            Method: get

Любой ресурс в шаблоне обладает уникальным логическим именем. Используя это имя, можно обращаться к свойствам этого ресурса. В моем примере логическое имя функции GetProjectByIdFunction. Переменная Runtime говорит о том, какую среду надо использовать для запуска кода. CodeUri и Handler определяют путь до файла с кодом и имя функции обработчика. Блок Events описывает виды событий, обрабатываемые функцией.

И последний блок, о котором мне надо сейчас знать — это блок Outputs, своего рода результат выполнения развертывания. Имеет смысл в этот раздел добавлять значения, которые могут потребоваться для дальнейшей работы или для интеграций, например, урлы созданных апишек, хостнейм поднятой БД и так далее. Amazon показывает эти значения в консоли после развертывания. Значения отображаются, как они есть, поэтому ни в коем случае не надо показывать в этом блоке пароли или секретные ключи.

Outputs:
  GetProjectByIdFunctionApi:
    Description: "ApiGateway endpoint URL for Prod stage for GetProjectByIdFunction"
    Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello/"
  GetProjectByIdFunction:
    Description: "GetProjectByIdFunction Lambda Function ARN"
    Value: !GetAtt GetProjectByIdFunction.Arn
  GetProjectByIdFunctionIamRole:
    Description: "Implicit IAM Role created for GetProjectByIdFunction function"
    Value: !GetAtt GetProjectByIdFunctionRole.Arn

Как видно из сниппетов, SAM также создает API, запросы которого обрабатываются лямбда функциями. В Outputs, помимо идентификаторов лямбда функции (все ресурсы в амазоне обладают уникальных идентификатором ARN), возвращается урл для взаимодействия с этим API. Пока сконцентрируюсь на лямбда функциях, к построению API вернусь позже.

Запуск лямбда функции

Напишу первую лямбда функцию, которая вернет фиксированные данные по идентификатору проекта:

// ./src/handlers/get-project-by-id/app.js
exports.lambdaHandler = async (event, context) => {
    return {
        id: 123,
        title: 'My first project',
        description: 'First project to work with serverless. No cards. No members.',
        cards: [],
        members: []
    };
};

AWS поддерживает несколько подходов в написании обработчиков на nodejs:

  • использование async/await конструкций. В этом случае обработчик может вернуть готовое значение или promise;

  • использование callback функций. В этом случае в обработчик добавляется третий аргумент callback, используя который, можно возвращать ответ;

  • представление обработчика в виде ES модуля (совсем недавно nodejs среда стала поддерживать эту возможность).

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

Вроде все готово для первого запуска. Я хочу иметь возможность запускать лямбда функции локально в режиме отладки, так я быстрее смогу искать, исправлять ошибки и проверить, что все работает, как надо. В SAM есть возможность запускать лямбды локально. Для локального запуска используется специальный докер образ от Amazon. Созданный таким образом контейнер мало отличается от среды выполнения в облаке. 

Запустить код локально можно несколькими способами.

Сборка и запуск при помощи SAM

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

Сборка приложения происходит при помощи команды:

serverless-bugtracker> sam build

Эта команда подготавливает артефакты для последующего развертывания или запуска. Результат сборки для каждой лямбда функции — это папка, в которой находятся все файлы из директории, прописанной в CodeUri в шаблоне. Если CodeUri отсутствует, то копируются все файлы из корневой директории. Также в этой папке устанавливаются все зависимости из package.json, который расположен в папке прописанной в CodeUri. 

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

Другой подход к структуре проекта

Встречаются и другие подходы к структуре проекта. Даже в примерах от Amazon можно встретить вариант, когда файлы всех функций находятся в одной папке с одним package.json, CodeUri параметр тоже указывает на одну и ту же директорию (либо вообще отсутствует). Пример проекта с несколькими функциями от Amazon:

Объявление функции в SAM шаблоне:

Resources:
  getAllItemsFunction:
    Type: AWS::Serverless::Function
    Properties:
      Handler: src/handlers/get-all-items.getAllItemsHandler
      Runtime: nodejs14.x
      ...
  getByIdFunction:
    Type: AWS::Serverless::Function
    Properties:
      Handler: src/handlers/get-by-id.getByIdHandler
      Runtime: nodejs14.x
      ...
  putItemFunction:
    Type: AWS::Serverless::Function
    Properties:
      Handler: src/handlers/put-item.putItemHandler
      Runtime: nodejs14.x
      ...

При таком подходе артефакты для разных функции будут идентичными. Например, первая лямбда работает с БД, а вторая работает с S3. В артефакте первой и второй лямбды будут обе зависимости. При таком подходе не получается полной изоляции, к тому же одни и те же зависимости повторяются по многу раз.

У команды sam build есть парочка полезных ключей. Ключ -p позволяет вести сборку параллельно. По умолчанию все ресурсы собираются последовательно. Ключ -c позволяет кешировать сборку. Если не происходило изменений в файлах, то в качестве артефакта будет использован закешированный артефакт. Если используется структура проекта с общей папкой для всех функций, то ключ -c работать не будет, SAM каждый раз будет пересобирать все заданные в шаблоне функции, поскольку всегда будет видеть изменения. Использование отдельных папок позволяет SAM четко определять изменения и собирать только измененные части приложения. В целом, эти ключи позволяют ускорить сборку и не делать лишнюю работу. 

По умолчанию сборка происходит в папку ./.aws-sam/build. Помимо артефактов функций, в ./.aws-sam/build есть еще template.yaml. Этот файл похож на мой файл из корня проекта, только все функции в нем настроены на сборочную директорию ./.aws-sam/build.

После выполнения команды сборки:

serverless-bugtracker> sam build -c -p

В консоли можно увидеть подсказки от SAM по дальнейшим действиям:

Build Succeeded

Built Artifacts  : .aws-sam\build
Built Template   : .aws-sam\build\template.yaml

Commands you can use next
=========================
[*] Invoke Function: sam local invoke
[*] Deploy: sam deploy --guided

Как видно из этой подсказки, для локального запуска функции необходимо использовать команду local invoke. Для запуска необходимо указать имя функции (логическое имя из шаблона). Опционально можно указать событие, которые должно быть обработано функцией. Для передачи события используется ключ -e. Если его не указать, то событие будет пустым. Можно указывать путь до файла с json объектом, представляющим это событие, или указать -, чтобы событие считывалось с stdin.

serverless-bugtracker> echo {"message": "Hey, are you there?" } | sam local invoke GetProjectByIdFunction  --event -

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

START RequestId: afebe87a-84a8-4b69-a430-08ec7f06828b Version: $LATEST
END RequestId: afebe87a-84a8-4b69-a430-08ec7f06828b
REPORT RequestId: afebe87a-84a8-4b69-a430-08ec7f06828b  Init Duration: 0.29 ms  Duration: 98.90 ms      Billed Duration: 100 ms   Memory Size: 128 MB     Max Memory Used: 128 MB
{"id":123,"title":"My first project","description":"First project to work with serverless. No cards. No members.","cards":[],"members":[]}

Запуск при помощи VSCode

Запускать локально функцию можно прямо из VSCode. Для этого мне потребуется плагин для VSCode AWS Toolkit. Этот плагин позволит быстрее деплоить из любимой среды разработки, ставить точки прерывания и инспектировать свой код. После установки необходимо его настроить — добавить креды аккаунта, чтобы плагин имел доступ к облаку. 

После установки плагина, над функцией появляется команда создания конфигурации запуска:

Плагин предлагает несколько режимов:

Первые две опции похожи, обе используют SAM шаблон, отличие только в типе события, с которым вызывается функция. В первом случае это может быть произвольное событие, во втором случае — это http запрос. VSCode создаст файл .vscode/launch.json, в котором будет настройка локального запуска лямбда функции. Я буду запускать лямбду первым способом, к работе с API вернусь в последующих статьях. Получится такой конфиг запуска в VSCode:

// .vscode/launch.json
{
    "configurations": [
        {
            "type": "aws-sam",
            "request": "direct-invoke",
            "name": "serverless-bugtracker:GetProjectByIdFunction",
            "invokeTarget": {
                "target": "template",
                "templatePath": "${workspaceFolder}/template.yaml",
                "logicalId": "GetProjectByIdFunction"
            },
            "lambda": {
                "payload": {
                    "json": {
                        "id": 123
                    }
                },
                "environmentVariables": {}
            }
        }
    ]
}

Третий вариант просто запускает код из директории, в этом случае игнорируется код в SAM шаблоне. Для работы третьего варианта потребуется установить компилятор typescript. Конфигурация этого запуска:

        {
            "type": "aws-sam",
            "request": "direct-invoke",
            "name": "get-project-by-id:app.lambdaHandler (nodejs14.x)",
            "invokeTarget": {
                "target": "code",
                "projectRoot": "${workspaceFolder}/src/handlers/get-project-by-id",
                "lambdaHandler": "app.lambdaHandler"
            },
            "lambda": {
                "runtime": "nodejs14.x",
                "payload": {
                    "json": {
                        "id": 123
                    }
                },
                "environmentVariables": {}
            }
        },

В секции payload можно указать json объект — событие, которое будет обработано функцией. Результат запуска любого из вариантов один и тот же —  приложение запускается локально в режиме отладки.

Плагин использует те же команды sam build и sam local invoke, только с другой сборочной директорией.

Развертывание в облако

Теперь я попробую сделать первый деплой в облако. Как и с локальным запуском, это можно сделать двумя способами. Первый способ — использовать консоль, второй — использовать плагин. Проверю оба варианта. 

Развертывание при помощи консольных команд

Попробуем сделать деплой, вооружившись консолью и SAM.

Прежде чем начать разворачивать приложение, необходимо его собрать. Команду я описал выше. 

Непосредственно для развертывания мне потребуется команда sam deploy. Команда sam deploy архивирует и копирует собранные артефакты в S3 бакет и разворачивает приложения в облаке. Все собранные ресурсы создаются внутри стека. CF стек (далее просто стек) это набор ресурсов в облаке, которые были развернуты при помощи CF шаблона. Каждый стек имеет уникальное для аккаунта имя. Первый раз лучше запустить команду с ключом --guided. Тогда запустится интерактивный режим, где потребуется ввести имя создаваемого стека, регион, имя S3 бакета для хранения кода и прочее. Но самое интересное в этом режиме то, что все выбранные опции можно сохранить в отдельный файл с определенным профилем (по умолчанию файл samconfig.toml, имя профиля по умолчанию default). Тогда в следующий раз не придется указывать никаких параметров, кроме имени профиля. Я сохранил все под профилем chapter1:

version = 0.1
[chapter1]
[chapter1.deploy]
[chapter1.deploy.parameters]
stack_name = "serverless-bugtracker-ch1"
s3_bucket = "serverless-bugtracker-sam"
s3_prefix = "serverless-bugtracker-ch1"
region = "us-east-1"
confirm_changeset = true
capabilities = "CAPABILITY_IAM"

В следующий раз для развертывания достаточно просто указать имя профиля:

serverless-bugtracker> sam deploy --config-env=chapter1

Развертывание при помощи плагина

При нажатии правой кнопкой мыши на template.yaml файл в контекстном меню появится новый пункт ”Deploy SAM Application”.

Плагин спросит:

  • какой template файл развернуть (у меня он только один):

  • aws регион:

  • s3 bucket, который SAM будет использовать. Можно создать новый:

  •  имя CF стека:

После прохождения всех шагов, AWS Toolkit запустит sam build, а потом sam deploy. Единственное отличие, что директория для сборки отличается от ./.aws-sam

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

В обоих случаях в конце в консоле я увидел заветную строчку Successfully deployed SAM Application to CloudFormation Stack: serverless-bugtracker-ch1. И вот моя лямбда крутится в облаке. Amazon сам придумал имя моей функции:

В SAM шаблоне у меня у функции нет никакого имени. Amazon берет имя стека, логическое имя функции в шаблоне и генерирует какую-то случайную последовательность символов. Избавиться от этого можно, если использовать свойство FunctionName в SAM шаблоне, тогда имя функции будет то, которое указать в этом поле. 

Функцию можно протестировать, используя AWS Console. Если открыть lambda функцию, у нее будет специальная вкладка test. Тут я могу послать любой json в качестве события (похоже на payload json, который я использовал в локальном запуске):

Подведение итогов

Подведем итог:

  • Сделал каркас приложения. Используется подход, при котором лямбда функции независимы и располагаются в разных директориях. 

  • Реализовал одну лямбда функцию. Она пока не очень умная и умеет возвращать мой фиксированный объект. 

  • Приложение создано при помощи SAM. Использую подход IaaC.

  • Для локальной разработки установлен плагин к VSCode, с помощью которого могу запустить лямбду локально в режиме отладки. 

  • Лямбду можно развернуть из среды разработки или при помощи консольных команд.

Код можно найти по ссылке.

В следующих главах я разверну БД и научу свою функцию взаимодействовать с базой данных. Создам реализации и для других функций.

P.S. Спасибо oN0 за помощь в написании статьи.

Теги:
Хабы:
Всего голосов 1: ↑1 и ↓0 +1
Просмотры 6.9K
Комментарии Комментарии 32

Информация

Дата основания
Местоположение
Россия
Сайт
www.lineate.ru
Численность
201–500 человек
Дата регистрации
Представитель