AWS

AWS Lambda та Serverless Compute

Глибоке дослідження AWS Lambda — від фундаментальних принципів serverless архітектури до продакшн-оптимізації .NET функцій. Runtimes, Layers, Triggers, Cold Start, Provisioned Concurrency, Lambda Destinations та повна інтеграція з екосистемою AWS для .NET 10 розробників.

AWS Lambda та Serverless Compute

Еволюція моделей обчислень: від серверів до функцій

Щоб зрозуміти місце AWS Lambda в сучасній хмарній архітектурі, необхідно простежити еволюційний шлях, який пройшла індустрія від фізичних серверів до парадигми «функції як сервіс». Кожен крок цієї еволюції вирішував конкретну проблему попереднього покоління, але водночас породжував нові класи складності.

Перше покоління: фізичні сервери (On-Premises). Організації купували та обслуговували власне апаратне забезпечення. Час від ухвалення рішення до введення сервера в експлуатацію вимірювався тижнями або місяцями. Ресурси були фіксованими: якщо сервер придбаний з 32 ГБ RAM, рівно стільки і доступно — незалежно від фактичного навантаження. Команди витрачали значну частину зусиль на управління інфраструктурою замість розробки бізнес-логіки.

Друге покоління: IaaS та EC2. AWS у 2006 році запровадила модель Infrastructure as a Service, де фізичний сервер замінюється віртуальною машиною, що надається за хвилини. Це вирішило проблему тривалого розгортання (provisioning) і дозволило платити лише за час використання. Проте фундаментальна проблема залишилась: команда все одно відповідає за операційну систему, патчі безпеки, мережеву конфігурацію, моніторинг процесів та масштабування.

Третє покоління: PaaS та контейнери. Платформи як Elastic Beanstalk або Amazon ECS абстрагували рівень операційної системи. Docker-контейнери вирішили проблему залежностей та відтворюваності середовища. Але команда все ще мала думати про кількість контейнерів, їхнє масштабування та час, протягом якого вони запущені.

Четверте покоління: Serverless та FaaS. AWS Lambda, запущена у 2014 році, реалізує принципово іншу модель: розробник пише лише функцію — одиницю бізнес-логіки, — а вся інфраструктурна відповідальність делегується платформі. Lambda не просто приховує сервери — вона усуває саму концепцію сервера з розумової моделі розробника.

Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

title "Еволюція моделей відповідальності: що керує команда"

rectangle "On-Premises" as OP #fca5a5 {
    rectangle "Застосунок" as APP1 #fee2e2
    rectangle "Runtime (.NET)" as RT1 #fee2e2
    rectangle "ОС + патчі" as OS1 #fee2e2
    rectangle "Гіпервізор" as HV1 #fee2e2
    rectangle "Мережа" as NET1 #fee2e2
    rectangle "Фізичний сервер" as HW1 #fee2e2
}

rectangle "EC2 (IaaS)" as EC #fde68a {
    rectangle "Застосунок" as APP2 #fee2e2
    rectangle "Runtime (.NET)" as RT2 #fee2e2
    rectangle "ОС + патчі" as OS2 #fee2e2
    rectangle "Гіпервізор" as HV2 #d1fae5
    rectangle "Мережа" as NET2 #d1fae5
    rectangle "Фізичний сервер" as HW2 #d1fae5
}

rectangle "ECS / Fargate\n(Containers)" as ECS #bbf7d0 {
    rectangle "Застосунок" as APP3 #fee2e2
    rectangle "Runtime (.NET)" as RT3 #fee2e2
    rectangle "ОС (контейнер)" as OS3 #d1fae5
    rectangle "Оркестрація" as HV3 #d1fae5
    rectangle "Мережа" as NET3 #d1fae5
    rectangle "Фізичний сервер" as HW3 #d1fae5
}

rectangle "Lambda\n(Serverless)" as LAM #dbeafe {
    rectangle "Бізнес-логіка\n(лише код функції)" as APP4 #fee2e2
    rectangle "Runtime (.NET 10)" as RT4 #d1fae5
    rectangle "ОС" as OS4 #d1fae5
    rectangle "Масштабування" as HV4 #d1fae5
    rectangle "Мережа" as NET4 #d1fae5
    rectangle "Фізичний сервер" as HW4 #d1fae5
}

note bottom of OP : Команда керує ВСІМ
note bottom of EC : AWS керує залізом
note bottom of ECS : AWS керує залізом + ОС
note bottom of LAM : Команда пише лише код функції.\nAWS керує всім іншим.

@enduml
Serverless ≠ «без серверів». Сервери існують — просто ними керує AWS. Термін «serverless» означає, що розробник більше не моделює сервери у своїй архітектурній свідомості: немає концепції «скільки серверів нам потрібно», «коли вони запущені», «як їх масштабувати».

Фундаментальна модель AWS Lambda

AWS Lambda — це обчислювальний сервіс, що виконує код у відповідь на події та автоматично управляє обчислювальними ресурсами, необхідними для цього виконання. Розробник надає код (функцію), Lambda забезпечує все інше: розгортання (provisioning) серверів, масштабування, моніторинг, реєстрацію логів.

Центральна концепція Lambda — функція (Lambda Function). Функція є одиницею деплойменту та виконання. Вона містить код, конфігурацію (пам'ять, timeout, змінні середовища) та IAM роль, що визначає її права доступу до інших AWS-сервісів.

Модель виконання: Event → Handler → Response

Кожне виклик Lambda відбувається за єдиною схемою: зовнішня система (trigger) генерує подію (event), Lambda-сервіс ініціює виконання функції та передає подію у handler — точку входу коду, — а handler повертає відповідь або завершується без повернення значення (для асинхронних тригерів).

Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

participant "Trigger\n(S3 / API GW / EventBridge)" as TR #fef3c7
participant "Lambda Service\n(AWS)" as LS #dbeafe
participant "Execution Environment\n(мікро-VM)" as EE #d1fae5
participant "Handler\n(.NET код)" as H #bbf7d0

TR -> LS : Подія (Event JSON)
LS -> EE : Створити / отримати Execution Environment
EE -> H : Десеріалізувати Event\n→ виклик Handler(event, context)

alt "Синхронний тригер (API Gateway)"
    H --> EE : return Response
    EE --> LS : Відповідь
    LS --> TR : HTTP Response
else "Асинхронний тригер (S3, EventBridge)"
    H --> EE : return (void / Task)
    EE --> LS : Успішне завершення
    note right of LS : Тригер не чекає відповіді.\nLambda підтвердила отримання.
end

@enduml

Ціноутворення: оплата за мілісекунди виконання

Модель ціноутворення Lambda кардинально відрізняється від EC2: оплата не за час, поки сервер запущений, а виключно за фактичний час виконання коду та кількість викликів.

Кількість викликів

$0.20 за 1 мільйон викликів

Безкоштовно: перший мільйон викликів щомісяця (постійно, не лише 12 місяців free tier)

Тривалість виконання

$0.0000166667 за GB-секунду

Тривалість округлюється до 1 мілісекунди

Безкоштовно: 400,000 GB-секунд щомісяця

Приклад розрахунку

Функція: 128 MB пам'яті, 200ms виконання, 10M викликів/місяць

Вартість: ~$1.67 (тривалість) + $1.80 (виклики) = ~$3.47/місяць

Порівняння з EC2. Один EC2 instance t3.small (2 vCPU, 2 GB) коштує ~$15/місяць і працює 24/7 незалежно від навантаження. Lambda з аналогічним навантаженням (10M коротких викликів) коштуватиме менше $5 і не генерує витрат у часи простою.

Lambda Limits: операційні обмеження

Lambda накладає жорсткі технічні обмеження, які є наслідком її архітектурної природи. Знання цих меж є обов'язковим для проектування рішень на Lambda.

Максимальний час виконання (Timeout)
integer required
900 секунд (15 хвилин) — абсолютна максимальна тривалість одного виклику. Після досягнення таймауту Lambda примусово завершує виконання. Для довготривалих задач необхідно використовувати AWS Step Functions або ECS.
Максимальний розмір пакету деплойменту
string
50 MB (ZIP, завантажений напряму) або 250 MB (ZIP, розпакований). Через S3 можна завантажити ZIP до 10 GB. Ця межа є ключовою при роботі з .NET, де розмір publish-артефакту може бути значним.
Пам'ять
integer
Від 128 MB до 10,240 MB (10 GB) з кроком 1 MB. CPU потужність масштабується пропорційно виділеній пам'яті: при 1,769 MB функція отримує 1 повний vCPU. При 3,538 MB — 2 vCPU.
Ephemeral Storage (/tmp)
string
Від 512 MB до 10,240 MB. Тимчасове сховище, доступне лише протягом виконання та потенційно між викликами у теплому середовищі (warm execution environment). Не є надійним постійним сховищем.
Паралелізм (Concurrency)
integer
За замовчуванням 1,000 одночасних виконань на рівні акаунту AWS у регіоні. Можна запросити збільшення через AWS Support. Кожен одночасний виклик — окреме Execution Environment.
Розмір payload
string
6 MB для синхронних викликів (Request + Response). 256 KB для асинхронних. Для передачі великих даних необхідно використовувати S3 як проміжне сховище.

Execution Environment: анатомія ізоляції

Для розуміння поведінки Lambda — зокрема феноменів Cold Start та Warm Start — необхідно детально вивчити концепцію Execution Environment (середовища виконання).

Коли Lambda-сервіс отримує виклик, він або повторно використовує наявне Execution Environment (якщо воно вільне та відповідає функції), або ініціалізує нове. Execution Environment — це ізольоване мікро-VM середовище на базі технології AWS Firecracker (відкрита технологія легковісної віртуалізації, розроблена AWS). Кожне Execution Environment забезпечує:

  • повну ізоляцію від інших функцій та інших викликів тієї ж функції;
  • стабільне середовище виконання: операційна система Amazon Linux 2, встановлений Runtime (наприклад, .NET 10), і ваш код;
  • персистентний стан між викликами у межах одного Execution Environment — файлову систему /tmp, статичні змінні та об'єкти, ініціалізовані поза handler-функцією.
Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

title "Lifecycle одного Execution Environment"

[*] --> Init : Lambda отримує ПЕРШИЙ виклик\n(Cold Start)

state Init {
    [*] --> DownloadCode : Завантаження ZIP/Image
    DownloadCode --> StartRuntime : Запуск .NET Runtime
    StartRuntime --> RunInitCode : Виконання коду ВНЕ handler\n(DI container, DB connections,\nstatic constructors)
    RunInitCode --> [*]
}

Init --> Invoke : Init завершено → Handler доступний

state Invoke {
    [*] --> RunHandler : Виклик Handler(event, context)
    RunHandler --> ReturnResult : Повернення результату
    ReturnResult --> [*]
}

Invoke --> Idle : Виклик завершено\n(Warm, очікує наступного)
Idle --> Invoke : Наступний виклик (Warm Start)\n← жодних затримок ініціалізації!
Idle --> [*] : Таймаут простою (~15-45 хвилин)\nAWS знищує Execution Environment

note right of Init
  Cold Start:
  Відбувається ЛИШЕ при першому виклику
  або після знищення середовища.
  Тривалість: від 100ms до кількох секунд
  (залежить від Runtime та розміру коду).
end note

note right of Idle
  Warm Start:
  Наступний виклик отримує вже готове
  середовище. Час ініціалізації = 0.
  Статичні об'єкти зберігаються між
  викликами (connection pooling!)
end note

@enduml

Cold Start vs Warm Start: детальний аналіз

Cold Start — це затримка, що виникає при ініціалізації нового Execution Environment. Вона складається з кількох фаз, кожна з яких додає свою частину до загального часу:

ФазаОписТиповий час
Download CodeЗавантаження ZIP-артефакту з S310–200ms
Start RuntimeЗапуск JVM, .NET CLR або інтерпретатора50–500ms
Run Init CodeВиконання static конструкторів, DI container10–5000ms
Run HandlerПерший виклик обробникачас функції

Критично важливо розуміти: Cold Start — не просто «перший виклик повільніший». Cold Start відбувається кожного разу, коли Lambda масштабується горизонтально. Якщо одночасно надходить 100 запитів, а є лише 10 теплих Execution Environments, Lambda ініціалізує 90 нових — 90 одночасних Cold Starts.

Cold Start для .NET — особлива проблема. .NET Runtime (CLR) має значний час ініціалізації порівняно з Python або Node.js. Нативний .NET 10 Cold Start типово займає 800ms–3s залежно від розміру застосунку та кількості ініціалізованих компонентів. Це є головним викликом для .NET Lambda розробників і вимагає спеціальних технік оптимізації.

Warm Start — виклик функції, коли Lambda-сервіс знаходить вже ініціалізоване (але вільне) Execution Environment. У цьому випадку фаза ініціалізації повністю пропускається, і виконання починається безпосередньо з Handler. Час Warm Start практично дорівнює часу виконання самої бізнес-логіки.

Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

title "Cold Start vs Warm Start: часова шкала виклику"

skinparam sequence {
    ParticipantBackgroundColor #f8fafc
}

participant "Виклик 1\n(Cold Start)" as C1
participant "Виклик 2\n(Warm Start)" as C2

note over C1
  **COLD START**
  Завантаження коду: 150ms ████████
  Старт .NET CLR: 400ms ████████████████████
  Init код (DI тощо): 600ms ████████████████████████████████
  Handler: 50ms ███
  ────────────────────────────────────
  ЗАГАЛОМ: ~1200ms
end note

note over C2
  **WARM START**
  (середовище вже готове)
  Handler: 50ms ███
  ────────────────────────────────────
  ЗАГАЛОМ: ~50ms

  Економія: 1150ms (95.8%)!
end note

@enduml

Lambda Runtimes: середовища виконання

Runtime — це компонент Execution Environment, що забезпечує взаємодію між Lambda-сервісом та кодом функції. Runtime відповідає за: отримання події від Lambda-сервісу, десеріалізацію JSON-payload у нативні об'єкти, виклик handler-функції та серіалізацію відповіді назад у JSON.

AWS надає керовані Runtime для більшості популярних мов програмування. У разі, коли жоден із керованих Runtime не підходить, розробник може створити Custom Runtime — довільний виконуваний файл, що реалізує Lambda Runtime API.

.NET 10

Identifier: dotnet10

Офіційний керований Runtime від AWS. LTS-версія .NET. Оптимальний вибір для нових .NET Lambda проектів. Підтримує Native AOT компіляцію для мінімізації Cold Start.

.NET 6

Identifier: dotnet6

Попередня LTS-версія. Підтримується до листопада 2024. Рекомендується міграція на .NET 10.

Node.js 20

Identifier: nodejs20.x

Найпопулярніший Runtime для serverless. Мінімальний Cold Start (~100ms). Ідеальний для простих обробників подій та glue-коду.

Python 3.12

Identifier: python3.12

Широко використовується у ML/Data pipelines та скриптах автоматизації. Дуже короткий Cold Start.

Java 21

Identifier: java21

Підтримує GraalVM Native Image для зменшення Cold Start. Аналогічна до .NET ситуація з часом ініціалізації JVM.

Custom Runtime

Identifier: provided.al2023

Будь-яка мова, що може скомпілюватися у Linux-бінарник (Rust, Go, C++, .NET Native AOT). Максимальна гнучкість та мінімальний Cold Start.

.NET Runtime для Lambda: специфіка та особливості

AWS надає Amazon.Lambda.Core NuGet-пакет, який є центральним компонентом для написання Lambda функцій на .NET. Він визначає інтерфейс ILambdaContext та базовий клас ILambdaSerializer.

Виклик Lambda-функції на .NET відбувається через механізм Function Handler — метод зі специфічною сигнатурою:

// Варіант 1: синхронний handler з поверненням значення
public TOutput FunctionHandler(TInput input, ILambdaContext context)

// Варіант 2: асинхронний handler (рекомендується для .NET)
public async Task<TOutput> FunctionHandlerAsync(TInput input, ILambdaContext context)

// Варіант 3: handler без вхідних даних
public async Task<TOutput> FunctionHandlerAsync(ILambdaContext context)

// Варіант 4: handler без вхідних даних та без повернення
public async Task FunctionHandlerAsync(ILambdaContext context)

ILambdaContext надає метаданих про поточний виклик:

AwsRequestId
string
Унікальний ідентифікатор поточного виклику. Використовується для кореляції логів у CloudWatch та для ідемпотентності (уникнення повторної обробки).
FunctionName
string
Назва Lambda-функції. Корисна при використанні одного handler для кількох функцій або для умовної логіки на основі контексту деплойменту.
RemainingTime
TimeSpan
Час, що залишився до досягнення Timeout. Критично важливо для реалізації graceful shutdown: функція може перевіряти RemainingTime та коректно завершувати роботу до примусового знищення.
MemoryLimitInMB
integer
Кількість пам'яті, виділеної для функції. Корисна для динамічного налаштування буферів або розміру пакетів обробки.
Logger
ILambdaLogger
Базовий логер, що записує у CloudWatch Logs. Підтримує рівні: LogTrace, LogDebug, LogInformation, LogWarning, LogError, LogCritical. Для продакшн-систем рекомендується використовувати Microsoft.Extensions.Logging з відповідним Lambda-провайдером.

Створення Lambda-проекту: від порожньої папки до Hello World

Для розробника .NET існує три основні шляхи створення проекту. Вибір залежить від того, чи хочете ви контролювати кожен файл, чи використати готові кращі практики від AWS.

Варіант А: Ручне створення (Manual) — для повного контролю

Це дозволяє зрозуміти, як саме працює Lambda без "магії" шаблонів.

  1. Створіть директорію та проект:
    mkdir MyLambda && cd MyLambda
    dotnet new classlib
    
  2. Додайте ядро SDK:
    dotnet add package Amazon.Lambda.Core
    dotnet add package Amazon.Lambda.Serialization.SystemTextJson
    
  3. Напишіть код: Створіть клас Function.cs з методом, який приймає string або об'єкт.
  4. Зареєструйте серіалізатор: Додайте атрибут [assembly: LambdaSerializer(...)] над namespace.

Варіант Б: Використання AWS Templates (Рекомендовано)

AWS надає набір шаблонів, які автоматично створюють правильну структуру, включаючи конфігураційні файли.

  1. Встановіть шаблони:
    dotnet new install Amazon.Lambda.Templates
    
  2. Перегляньте список доступних типів:
    dotnet new lambda --list
    
  3. Створіть проект (наприклад, для S3 подій):
    dotnet new lambda.S3 -n MyS3Processor
    

Структура проекту: Файл aws-lambda-tools-defaults.json

При створенні через шаблони ви знайдете цей файл. Він є критично важливим, бо зберігає налаштування для dotnet lambda cli.

{
    "Information": "This file saves default values for the dotnet lambda CLI.",
    "profile": "default",
    "region": "eu-central-1",
    "configuration": "Release",
    "framework": "net10.0",
    "function-runtime": "dotnet10",
    "function-memory-size": 256,
    "function-timeout": 30,
    "function-handler": "MyProject::MyProject.Function::Handler"
}

Завдяки цьому файлу вам не потрібно щоразу вводити довгі параметри в терміналі.


Lambda Layers: механізм спільного використання коду

Lambda Layer — це ZIP-архів, що містить бібліотеки, кастомний Runtime, конфігурацію або будь-які інші файли, які можна розділити між кількома Lambda-функціями. Layer монтується в директорію /opt Execution Environment і стає доступним для функції під час виконання.

Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

title "Lambda Layers: структура та монтування"

package "Layer 1: Shared .NET Libraries (v2.3)" as L1 #dbeafe {
    rectangle "Newtonsoft.Json 13.0" as NJ
    rectangle "Serilog + Sinks" as SL
    rectangle "FluentValidation 11.x" as FV
}

package "Layer 2: AWS SDK Extensions (v1.1)" as L2 #fef3c7 {
    rectangle "AWSSDK.S3" as S3LIB
    rectangle "AWSSDK.DynamoDBv2" as DYNLIB
    rectangle "Amazon.Lambda.Core" as LCORE
}

package "Execution Environment" as EE #f0fdf4 {
    rectangle "/var/task\n(Код функції — лише ваш код!)" as TASK #bbf7d0
    rectangle "/opt/dotnet/packages\n(Layer 1 + Layer 2)" as OPT #d1fae5
    rectangle ".NET Runtime" as DOTNET
}

L1 -down-> OPT : Монтується при ініціалізації
L2 -down-> OPT : Монтується при ініціалізації
TASK -right-> OPT : Імпортує бібліотеки

note bottom of EE
  Без Layers: ZIP функції = 45MB
  З Layers: ZIP функції = 2MB
  Бібліотеки оновлюються незалежно
  від коду функції!
end note

@enduml

Технічна реалізація для .NET

Коли ви додаєте Layer до Lambda, AWS розпаковує його в папку /opt. Для того, щоб .NET Runtime міг знайти ваші бібліотеки (DLL), вони мають бути розміщені у специфічній підпапці.

Шлях пошуку за замовчуванням: /opt/dotnet/packages

Коли .NET Lambda ініціалізується, вона автоматично додає цей шлях до DOTNET_SHARED_STORE або використовує механізм Assembly Resolution для пошуку залежностей у цій локації.

Переваги використання Layers

Зменшення пакетів

Виносьте важкі NuGet-пакети (наприклад, Entity Framework або ImageSharp) у шари. Це скорочує час завантаження коду функції з S3 під час Cold Start.

Централізація

Змінюйте версію спільної бібліотеки в одному шарі, замість того щоб перезбирати та деплоїти 50 різних функцій.

Розподіл праці

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

Mini-Workshop: Створення та деплой Layer для .NET

Розглянемо процес винесення бібліотеки Newtonsoft.Json у окремий шар.

Підготовка структури

Створіть папку зі специфічною структурою для .NET:

mkdir -p my-layer/dotnet/packages
cd my-layer/dotnet/packages

Публікація залежностей

Створіть тимчасовий проект і опублікуйте його DLL у цю папку:

dotnet new classlib -n TempLayer
dotnet add package Newtonsoft.Json
dotnet publish -c Release -o .

Після цього видаліть зайві файли (.csproj, .json), залишивши лише потрібні .dll.

Створення та завантаження шару

# Пакуємо папку 'dotnet' у ZIP
cd ../..
zip -r my-layer.zip dotnet/

# Завантажуємо в AWS
aws lambda publish-layer-version \
    --layer-name SharedLibraries \
    --description "Common NuGet packages" \
    --zip-file fileb://my-layer.zip \
    --compatible-runtimes dotnet10

Підключення до функції

Тепер у конфігурації вашої Lambda додайте ARN отриманого шару:

aws lambda update-function-configuration \
    --function-name MyFunction \
    --layers arn:aws:lambda:eu-central-1:123456789012:layer:SharedLibraries:1
Ліміти Layers. Ви можете підключити до 5 шарів одночасно. Сумарний розпакований розмір коду функції + всіх шарів не повинен перевищувати 250 MB.

AWS SDK for Lambda: Глибоке занурення в екосистему .NET пакетів

Для ефективної розробки Lambda-функцій на .NET недостатньо базового знання мови; необхідно розуміти структуру та призначення офіційних бібліотек від AWS. Екосистема SDK для Lambda складається з трьох рівнів: ядро (Core), серіалізація (Serialization) та бібліотеки подій (Event Libraries).

1. Фундаментальні пакети (Core & Serialization)

Ці пакети є обов'язковими для будь-якого проекту Lambda.

Amazon.Lambda.Core

Визначає базові інтерфейси: ILambdaContext, ILambdaSerializer та базові атрибути логування. Без цього пакету неможливо створити коректну сигнатуру Handler.

Amazon.Lambda.Serialization.SystemTextJson

Реалізує логіку перетворення вхідного JSON від AWS-сервісів у ваші C# об'єкти. Використовує високоефективний System.Text.Json.

Механізм реєстрації серіалізатора: AWS Lambda не знає за замовчуванням, як десеріалізувати вхідні дані. Ви ПОВИННІ вказати серіалізатор на рівні assembly (найкраща практика) або конкретного методу:

// Реєстрація на рівні всього проекту (assembly)
[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))]

2. Бібліотеки подій (Event Libraries)

AWS не передає "просто JSON". Кожен сервіс-тригер має строго визначену схему даних. Пакети серії Amazon.Lambda.*Events містять готові класи (DTO), які відповідають цим схемам.

Сервіс-тригерПакет NuGetКлючовий клас події
API Gateway (HTTP)Amazon.Lambda.APIGatewayEventsAPIGatewayHttpApiV2ProxyRequest
S3 StorageAmazon.Lambda.S3EventsS3Event
Simple Queue (SQS)Amazon.Lambda.SQSEventsSQSEvent
DynamoDB StreamsAmazon.Lambda.DynamoDBEventsDynamoDBEvent
EventBridge / CronAmazon.Lambda.CloudWatchEventsScheduledEvent

3. Анатомія ILambdaContext

Об'єкт context, що передається у кожен виклик, є "вікном" у Runtime-середовище Lambda. Він надає метадані, необхідні для моніторингу та управління життєвим циклом виконання.

AwsRequestId
string
Унікальний ідентифікатор запису (GUID). Це найважливіше поле для відладки. Завжди логуйте його на початку обробки для кореляції логів.
RemainingTime
TimeSpan
Динамічний лічильник часу до примусового завершення (Timeout).
  • Сценарій: якщо ви обробляєте батч з 100 елементів, перевіряйте context.RemainingTime після кожного елемента. Якщо залишилось < 2 секунд — зупиніться і збережіть стан. ::
Logger
ILambdaLogger
Прямий доступ до CloudWatch Logs. Методи LogLine та Log пишуть у стандартний потік, додаючи метадані Lambda.
InvokedFunctionArn
string
Повний ARN функції, включаючи Alias або Version (наприклад, ...:function:my-func:PROD). Дозволяє коду знати, в якому середовищі він запущений.

::

4. Просунута розробка: Lambda Annotations (Повна документація)

Фреймворк AWS Lambda Annotations (Amazon.Lambda.Annotations) — це сучасна модель програмування для .NET Lambda, яка кардинально спрощує створення REST та HTTP API. Замість того, щоб вручну парсити JSON-запити через низькорівневий об'єкт APIGatewayProxyRequest, ви використовуєте звичні атрибути в стилі ASP.NET Core Minimal APIs.

Як це працює під капотом?

Фреймворк використовує C# Source Generators. Під час компіляції вашого коду (Build), він автоматично:

  1. Аналізує ваші методи, помічені атрибутом [LambdaFunction].
  2. Генерує "приховані" обгортки (wrapper classes), які виконують всю "брудну" роботу: десеріалізацію вхідних HTTP-запитів, обробку помилок та форматування HTTP-відповідей.
  3. Автоматично оновлює файл serverless.template (шаблон AWS CloudFormation / SAM), записуючи туди всі маршрути (routes) та конфігурацію інфраструктури.

Ключові атрибути для маршрутизації

Фреймворк надає атрибути для прив'язки методів до API Gateway:

  • [LambdaFunction(ResourceName = "MyFunc", MemorySize = 512)] — перетворює метод на Lambda-функцію. Можна задати пам'ять, таймаут, ролі.
  • [HttpApi(LambdaHttpMethod.Get, "/users/{id}")] — реєструє маршрут для сучасного та швидкого HTTP API.
  • [RestApi(LambdaHttpMethod.Post, "/orders")] — реєструє маршрут для класичного REST API (з підтримкою WAF, Usage Plans).

Біндинг параметрів (Parameter Binding)

Подібно до ASP.NET, ви можете вказувати, звідки брати дані, за допомогою атрибутів:

  • [FromQuery] string status — з URL параметру (?status=active).
  • [FromRoute] int id — зі змінної в шляху (/users/{id}).
  • [FromBody] Order order — автоматично десеріалізує JSON-тіло запиту.
  • [FromHeader("Authorization")] string token — з HTTP-заголовка.

Приклад коду:

[LambdaFunction(MemorySize = 256)]
[HttpApi(LambdaHttpMethod.Post, "/users/{userId}/roles")]
public async Task<IHttpResult> AssignRole(
    [FromRoute] string userId,
    [FromQuery] bool notify,
    [FromBody] RoleRequest request,
    ILambdaContext context) // ILambdaContext завжди доступний
{
    // Ваша логіка...
    return HttpResults.Ok(new { success = true, user = userId, role = request.Role });
}

Впровадження залежностей (Dependency Injection)

У звичайній Lambda налаштування DI-контейнера є неочевидним завданням. З Annotations ви створюєте клас із атрибутом [LambdaStartup], який виглядає точнісінько як Startup.cs в ASP.NET:

[LambdaStartup]
public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        // Ці сервіси будуть автоматично ініціалізовані під час Cold Start
        services.AddAWSService<IAmazonDynamoDB>();
        services.AddScoped<IUserRepository, UserRepository>();
    }
}

Після цього ви можете використовувати ін'єкцію через конструктор (Constructor Injection) у класі вашої функції.

Особливість деплойменту: Оскільки Lambda Annotations генерує конфігурацію у файл serverless.template, ви не можете деплоїти такі функції звичайною командою dotnet lambda deploy-function. Ви ПОВИННІ використовувати команду деплою всього стеку: dotnet lambda deploy-serverless --stack-name my-api-stack --s3-bucket my-bucket --region eu-central-1

5. SDK у дії: Практичні приклади реалізації

Розглянемо поширені сценарії використання SDK для вирішення типових задач.

А. Логування та контекст виконання

Використання ILambdaContext для відстеження стану та лімітів.

public async Task<string> Handler(string input, ILambdaContext context)
{
    // 1. Логування з метаданими
    context.Logger.LogInformation($"Request ID: {context.AwsRequestId}");
    context.Logger.LogInformation($"Remaining Time: {context.RemainingTime.TotalMilliseconds}ms");

    // 2. Логіка захисту від таймауту
    if (context.RemainingTime < TimeSpan.FromSeconds(1))
    {
         context.Logger.LogWarning("Running out of time! Aborting...");
         return "Timeout Imminent";
    }

    return $"Processed: {input}";
}

Б. Десеріалізація складних об'єктів (POCO)

Завдяки серіалізатору SystemTextJson ви можете приймати будь-які C# класи.

public record UserRequest(string Name, int Age, List<string> Roles);
public record UserResponse(string Message, bool IsAdult);

public UserResponse HandleUser(UserRequest request, ILambdaContext context)
{
    context.Logger.LogInformation($"Processing user: {request.Name}");

    return new UserResponse(
        Message: $"Hello, {request.Name}!",
        IsAdult: request.Age >= 18
    );
}

Lambda Triggers: архітектура подієво-орієнтованої інтеграції

У хмарній екосистемі AWS Lambda ніколи не працює у вакуумі. Функція завжди ініціюється певною подією (Event) ззовні. Цей механізм ініціалізації називається Trigger (тригер).

Сучасні архітектури використовують подієво-орієнтований підхід (Event-Driven Architecture), де сервіси не викликають один одного напряму, а реагують на зміни стану. AWS пропонує понад 200 нативних інтеграцій з Lambda. Проте, незважаючи на таку кількість, архітектурно всі вони поділяються на дві фундаментальні моделі доставки подій: Push-модель (коли сервіс сам "штовхає" подію в Lambda) та Pull-модель (коли Lambda "витягує" події з джерела).

Важливо розуміти, що Push-модель додатково ділиться на синхронну та асинхронну. Ваш підхід до обробки помилок, ретраїв та масштабування буде кардинально відрізнятися залежно від обраної моделі.

1. Модель Push: Синхронний виклик (RequestResponse)

У цій моделі сервіс-тригер (або клієнт) робить прямий HTTP-запит до Lambda Service API і блокується, очікуючи на відповідь.

  • Хто керує помилками: Клієнт. Якщо функція впала або сталася помилка таймауту, AWS Lambda не буде робити автоматичний retry. Клієнт отримує 5xx помилку і сам вирішує, чи повторювати запит.
  • Типові сервіси: Amazon API Gateway, Application Load Balancer, Amazon Cognito, або прямий виклик через AWS SDK (InvokeType = RequestResponse).
  • Use-case: HTTP API, де користувачу потрібна миттєва відповідь (наприклад, отримання профілю користувача).

2. Модель Push: Асинхронний виклик (Event)

У цій моделі сервіс-тригер відправляє подію до внутрішньої черги AWS Lambda Service і відразу отримує підтвердження "Подія прийнята" (HTTP 202). Він не чекає на завершення виконання коду вашої функції.

  • Хто керує помилками: AWS Lambda Service. Внутрішня система автоматично робить 2 повторні спроби (Retries) з експоненційною затримкою (backoff), якщо ваш код кидає exception. Якщо всі спроби вичерпано, подія може бути відправлена у Dead Letter Queue (DLQ) або Lambda Destination.
  • Типові сервіси: Amazon S3, Amazon SNS, Amazon EventBridge.
  • Use-case: Фонова обробка. Наприклад, користувач завантажив відео в S3, і нам потрібно згенерувати прев'ю. Користувач не повинен чекати 3 хвилини; S3 асинхронно тригерить Lambda.

3. Модель Pull: Event Source Mapping (Poll-based)

Тут Lambda Service виступає в ролі активного "споживача". Спеціальний компонент (Event Source Mapping) безперервно опитує (polls) джерело даних на наявність нових записів, групує їх у "батчі" (Batches) і відправляє у вашу функцію одним синхронним викликом.

  • Хто керує помилками: Event Source Mapping. Якщо ваша функція падає під час обробки батчу, весь батч повертається в чергу або потік, і AWS буде намагатися передати його знову (поки не вийде термін дії повідомлення, або поки ви не налаштуєте часткову обробку відмов — ReportBatchItemFailures).
  • Типові сервіси: Amazon SQS, DynamoDB Streams, Amazon Kinesis, Kafka (MSK).
  • Use-case: Обробка великих потоків даних, де важливий порядок повідомлень або потрібно контролювати швидкість (throttling), з якою Lambda споживає дані (через налаштування Batch Size).
Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

title "Дві моделі виклику Lambda: синхронна та асинхронна"

package "Синхронні тригери\n(Invoke Type: RequestResponse)" as SYNC #dbeafe {
    rectangle "API Gateway\n(HTTP API / REST API)" as APIGW #bbf7d0
    rectangle "Application\nLoad Balancer" as ALBT #bbf7d0
    rectangle "Amazon Cognito\n(Pre/Post triggers)" as COG #bbf7d0
    rectangle "AWS SDK\n(прямий виклик)" as SDK #bbf7d0
}

package "Асинхронні тригери\n(Invoke Type: Event)" as ASYNC #fef3c7 {
    rectangle "Amazon S3\n(Object events)" as S3T #fde68a
    rectangle "Amazon SNS\n(Push notifications)" as SNS #fde68a
    rectangle "EventBridge\n(Scheduled / Rules)" as EB #fde68a
    rectangle "CloudWatch Logs\n(Log subscriptions)" as CWL #fde68a
}

package "Poll-based тригери\n(Lambda опитує чергу)" as POLL #f3e8ff {
    rectangle "Amazon SQS\n(Standard / FIFO)" as SQST #e9d5ff
    rectangle "DynamoDB Streams\n(Change Data Capture)" as DYNSTR #e9d5ff
    rectangle "Amazon Kinesis\n(Data Streams)" as KIN #e9d5ff
    rectangle "Amazon MSK / Kafka\n(Event Source Mapping)" as MSK #e9d5ff
}

rectangle "Lambda Function\n.NET 10" as LAM #d1fae5

SYNC --> LAM : Виклик + очікування відповіді\n(клієнт блокується)
ASYNC --> LAM : Подія поставлена у чергу Lambda\n(клієнт НЕ чекає)
POLL --> LAM : Lambda сама читає батч записів\nз черги або потоку

@enduml

Синхронний тригер: API Gateway + Lambda

Інтеграція Amazon API Gateway з Lambda є фундаментальним патерном побудови serverless HTTP API. API Gateway виступає проксі, що приймає HTTP-запити від клієнтів, трансформує їх у Lambda-події та повертає Lambda-відповідь клієнту.

AWS надає два типи API Gateway для інтеграції з Lambda:

HTTP API (v2)

Рекомендований вибір для більшості .NET Lambda сценаріїв.

  • Нижча вартість (~70% дешевше за REST API)
  • Нижча латентність
  • Спрощена конфігурація
  • Підтримка JWT авторизації «з коробки»
  • Обмежена функціональність (немає трансформацій запитів/відповідей)

REST API (v1)

Для складних сценаріїв з потребою у:

  • Трансформації request/response (Mapping Templates)
  • API Keys та Usage Plans
  • Детального контролю над схемою запитів (Request Validation)
  • AWS WAF інтеграції на рівні API

Структура події від API Gateway (HTTP API):

Коли клієнт надсилає GET /users/42?format=json, API Gateway трансформує запит у таку JSON-структуру, яку Lambda отримує як event:

{
    "version": "2.0",
    "routeKey": "GET /users/{id}",
    "rawPath": "/users/42",
    "rawQueryString": "format=json",
    "headers": {
        "accept": "application/json",
        "authorization": "Bearer eyJ...",
        "content-type": "application/json",
        "x-forwarded-for": "203.0.113.5"
    },
    "queryStringParameters": {
        "format": "json"
    },
    "pathParameters": {
        "id": "42"
    },
    "requestContext": {
        "accountId": "123456789012",
        "apiId": "abc123",
        "http": {
            "method": "GET",
            "path": "/users/42",
            "sourceIp": "203.0.113.5"
        },
        "requestId": "JKJaXmPLvHcESHA=",
        "stage": "$default"
    },
    "body": null,
    "isBase64Encoded": false
}

Реалізація HTTP API handler на .NET 10 з Lambda Annotations:

Lambda Annotations — це фреймворк від AWS, що генерує boilerplate-код через Source Generators і дозволяє писати Lambda-функції у стилі ASP.NET Core Minimal API:

Functions.cs
using Amazon.Lambda.Annotations;
using Amazon.Lambda.Annotations.APIGateway;
using Amazon.Lambda.Core;
using Microsoft.Extensions.Logging;

// Атрибут реєструє assembly як Lambda-функцію
[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))]

namespace MyApi.Lambda;

// У стандартній Lambda вам довелося б вручну створювати ресурси в AWS (наприклад, через веб-консоль)
// та налаштовувати, який URL викликає яку функцію.
// Завдяки фреймворку Lambda Annotations ви пишете код у стилі ASP.NET Core Controller.
// Під час компіляції генератор коду сам створить конфігурацію інфраструктури
// (CloudFormation шаблон) та налаштує всі URL-маршрути (routing) автоматично.
public class UserFunctions
{
    private readonly IUserRepository _repository;
    private readonly ILogger<UserFunctions> _logger;

    // DI через конструктор — повністю підтримується!
    public UserFunctions(IUserRepository repository, ILogger<UserFunctions> logger)
    {
        _repository = repository;
        _logger = logger;
    }

    // GET /users/{id} → HTTP API Lambda Integration
    [LambdaFunction(ResourceName = "GetUser")]
    [HttpApi(LambdaHttpMethod.Get, "/users/{id}")]
    public async Task<IHttpResult> GetUserAsync(
        int id,
        ILambdaContext context)
    {
        _logger.LogInformation(
            "GetUser called. RequestId={RequestId}, UserId={UserId}",
            context.AwsRequestId, id);

        var user = await _repository.GetByIdAsync(id);

        if (user is null)
            return HttpResults.NotFound(new { message = $"User {id} not found" });

        return HttpResults.Ok(user);
    }

    // POST /users → створення нового користувача
    [LambdaFunction(ResourceName = "CreateUser")]
    [HttpApi(LambdaHttpMethod.Post, "/users")]
    public async Task<IHttpResult> CreateUserAsync(
        [FromBody] CreateUserRequest request,
        ILambdaContext context)
    {
        // Перевірка RemainingTime для graceful shutdown
        if (context.RemainingTime < TimeSpan.FromSeconds(2))
        {
            _logger.LogWarning("Approaching timeout, aborting operation");
            return HttpResults.InternalServerError();
        }

        var user = await _repository.CreateAsync(request);
        return HttpResults.Created($"/users/{user.Id}", user);
    }
}

Налаштування Startup (DI Container) для Lambda:

Startup.cs
using Amazon.Lambda.Annotations;
using Microsoft.Extensions.DependencyInjection;

namespace MyApi.Lambda;

// LambdaStartup — аналог Program.cs для Lambda Annotations
[LambdaStartup]
public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        // Реєстрація сервісів — виконується ОДНОРАЗОВО при Cold Start
        // Всі наступні виклики (Warm Start) повторно використовують цей контейнер
        services.AddAWSService<IAmazonDynamoDB>();
        services.AddScoped<IUserRepository, DynamoDbUserRepository>();

        // HttpClient з правильним connection pooling для Lambda
        services.AddHttpClient("PaymentApi", client =>
        {
            client.BaseAddress = new Uri(Environment.GetEnvironmentVariable("PAYMENT_API_URL")!);
            client.Timeout = TimeSpan.FromSeconds(10);
        });

        services.AddLogging(logging =>
        {
            logging.AddLambdaLogger();
            logging.SetMinimumLevel(LogLevel.Information);
        });
    }
}
DI Container у Lambda. Завдяки persistence Execution Environment між Warm Start викликами, IServiceCollection реєструється лише один раз — під час Cold Start. Усі Scoped та Transient сервіси при кожному виклику створюються заново (що є коректною поведінкою), але Singleton'и живуть протягом усього часу існування Execution Environment.

Асинхронний тригер: S3 Events

Інтеграція з Amazon S3 є одним із найпоширеніших сценаріїв використання Lambda. S3 автоматично надсилає повідомлення Lambda, коли в bucket відбуваються певні дії: завантаження об'єкта (s3:ObjectCreated:*), видалення (s3:ObjectRemoved:*), відновлення з Glacier та інші.

Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

title "S3 → Lambda: асинхронна обробка файлів"

actor "Клієнт" as CL
rectangle "Amazon S3\nsource-images-bucket" as S3SRC #fef3c7
rectangle "Lambda Function\nImageProcessorFunction" as LAM #d1fae5
rectangle "Amazon S3\nprocessed-images-bucket" as S3DST #dbeafe
rectangle "Amazon DynamoDB\nimage-metadata" as DYN #f3e8ff

CL -> S3SRC : PUT image.jpg\n(оригінальне зображення)
S3SRC -> LAM : S3 Event\n(ObjectCreated)\n[асинхронно]
note right of S3SRC
  S3 підтверджує завантаження
  клієнту НЕГАЙНО,
  не чекаючи обробки Lambda
end note
LAM -> S3SRC : GetObject\n(завантажує оригінал)
LAM -> LAM : Resize / Compress\n/ Watermark
LAM -> S3DST : PutObject\n(оброблене зображення)
LAM -> DYN : PutItem\n(метадані: розмір, хеш, URL)

@enduml

Структура S3 Event, що надходить у Lambda:

{
    "Records": [
        {
            "eventVersion": "2.1",
            "eventSource": "aws:s3",
            "awsRegion": "eu-central-1",
            "eventTime": "2024-03-15T10:30:00.000Z",
            "eventName": "ObjectCreated:Put",
            "s3": {
                "s3SchemaVersion": "1.0",
                "bucket": {
                    "name": "source-images-bucket",
                    "arn": "arn:aws:s3:::source-images-bucket"
                },
                "object": {
                    "key": "uploads/user-123/profile.jpg",
                    "size": 2048576,
                    "eTag": "d8e8fca2dc0f896fd7cb4cb0031ba249"
                }
            }
        }
    ]
}

Зверніть увагу: S3 Event завжди містить масив Records, і теоретично може містити більше одного запису (хоча на практиці S3 зазвичай надсилає по одному запису). Обробник повинен ітерувати по всіх записах.

Handler для обробки S3 подій на .NET 10:

ImageProcessorFunction.cs
using Amazon.Lambda.Core;
using Amazon.Lambda.S3Events;
using Amazon.S3;
using Amazon.S3.Model;

[assembly: LambdaSerializer(
    typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))]

namespace ImageProcessor.Lambda;

public class ImageProcessorFunction
{
    // IAmazonS3 ініціалізується ОДНОРАЗОВО при Cold Start
    // та повторно використовується між Warm Start викликами
    private static readonly IAmazonS3 S3Client = new AmazonS3Client();
    private const string DestinationBucket = "processed-images-bucket";

    public async Task FunctionHandler(S3Event evnt, ILambdaContext context)
    {
        // S3Event може містити кілька Records — обробляємо всі
        foreach (var record in evnt.Records)
        {
            var srcBucket = record.S3.Bucket.Name;
            var srcKey = Uri.UnescapeDataString(record.S3.Object.Key.Replace("+", " "));

            context.Logger.LogInformation(
                "Processing: s3://{Bucket}/{Key} ({Size} bytes)",
                srcBucket, srcKey, record.S3.Object.Size);

            try
            {
                // Завантажуємо оригінальний файл з S3
                var getRequest = new GetObjectRequest
                {
                    BucketName = srcBucket,
                    Key = srcKey
                };

                using var response = await S3Client.GetObjectAsync(getRequest);
                using var inputStream = response.ResponseStream;

                // Обробка зображення (resize, compress тощо)
                using var processedStream = await ProcessImageAsync(inputStream, context);

                // Зберігаємо результат у destination bucket
                var dstKey = $"processed/{srcKey}";
                var putRequest = new PutObjectRequest
                {
                    BucketName = DestinationBucket,
                    Key = dstKey,
                    InputStream = processedStream,
                    ContentType = "image/webp",
                    // Server-side encryption
                    ServerSideEncryptionMethod = ServerSideEncryptionMethod.AES256
                };

                await S3Client.PutObjectAsync(putRequest);

                context.Logger.LogInformation(
                    "Successfully processed: s3://{Bucket}/{Key}",
                    DestinationBucket, dstKey);
            }
            catch (Exception ex)
            {
                context.Logger.LogError(ex,
                    "Failed to process s3://{Bucket}/{Key}",
                    srcBucket, srcKey);
                // При асинхронному тригері — Lambda повторить виклик
                // (до 2 разів за замовчуванням)
                throw;
            }
        }
    }

    private static async Task<Stream> ProcessImageAsync(
        Stream input, ILambdaContext context)
    {
        // ... логіка обробки зображення
        // Можна використовувати SixLabors.ImageSharp або аналоги
        return await Task.FromResult(input); // placeholder
    }
}

Poll-based тригер: SQS та DynamoDB Streams

Poll-based тригери реалізуються через механізм Event Source Mapping — компонент Lambda-сервісу, що безперервно опитує джерело подій (SQS черга, DynamoDB Stream, Kinesis Stream) та автоматично викликає Lambda при появі нових записів.

Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

title "Event Source Mapping: Lambda опитує SQS чергу"

rectangle "Producer\n(.NET API)" as PROD #fef3c7
rectangle "Amazon SQS\norder-processing-queue\n(Standard Queue)" as SQS #fde68a
rectangle "Event Source\nMapping\n(AWS Lambda Service)" as ESM #dbeafe
rectangle "Lambda Function\nOrderProcessorFunction" as LAM #d1fae5
rectangle "Amazon RDS\nPostgreSQL" as RDS #f3e8ff

PROD -> SQS : SendMessage\n(OrderCreated JSON)
ESM -> SQS : ReceiveMessage\n(polling кожні 1-20 сек)
SQS --> ESM : Batch of messages\n(до 10 000 повідомлень)
ESM -> LAM : Invoke з батчем\n(SQSEvent)
LAM -> RDS : INSERT INTO orders
LAM --> ESM : success
ESM -> SQS : DeleteMessage\n(видаляє успішно оброблені)

note right of ESM
  ESM автоматично:
  - масштабує кількість
    паралельних викликів
  - повторює невдалі батчі
  - моніторить метрики черги
end note

note bottom of SQS
  Повідомлення залишаються
  у черзі до підтвердження
  успішної обробки.
  Visibility Timeout: 300s
end note

@enduml

Конфігурація Event Source Mapping для SQS:

BatchSize
integer
Кількість повідомлень SQS, що передаються в один виклик Lambda. Від 1 до 10,000. Більший батч — менше викликів Lambda (дешевше), але якщо один запис невалідний — весь батч потрібно повторити. Рекомендоване значення: 10–100 для типових сценаріїв.
MaximumBatchingWindowInSeconds
integer
Час очікування накопичення батчу до MaxBatchSize. Lambda чекає до цього значення (0–300 секунд) перш ніж викликати функцію, навіть якщо батч ще не заповнений. Дозволяє зменшити кількість викликів при нерівномірному потоці.
FunctionResponseTypes
array
ReportBatchItemFailures — дозволяє Lambda повертати часткові успіхи. Замість відкидання всього батчу при помилці одного запису, функція може повернути список batchItemFailures, і лише вказані записи повернуться у чергу для повторної обробки.
BisectBatchOnFunctionError
boolean
При true: якщо Lambda завершилась з помилкою, ESM автоматично ділить батч навпіл та повторює кожну половину окремо — рекурсивно, доки не знайде проблемне повідомлення. Корисно при відсутності ReportBatchItemFailures.

Handler для SQS з підтримкою часткових збоїв (ReportBatchItemFailures):

OrderProcessorFunction.cs
using Amazon.Lambda.Core;
using Amazon.Lambda.SQSEvents;

namespace OrderProcessor.Lambda;

public class OrderProcessorFunction
{
    private readonly IOrderService _orderService;

    public OrderProcessorFunction(IOrderService orderService)
    {
        _orderService = orderService;
    }

    // Повертаємо SQSBatchResponse замість void/Task
    // щоб Lambda знала, які записи потрібно повторити
    public async Task<SQSBatchResponse> FunctionHandler(
        SQSEvent sqsEvent,
        ILambdaContext context)
    {
        var batchItemFailures = new List<SQSBatchResponse.BatchItemFailure>();

        // Обробляємо кожне повідомлення НЕЗАЛЕЖНО
        foreach (var message in sqsEvent.Records)
        {
            try
            {
                context.Logger.LogInformation(
                    "Processing SQS message: {MessageId}", message.MessageId);

                var order = System.Text.Json.JsonSerializer
                    .Deserialize<OrderCreatedEvent>(message.Body)!;

                await _orderService.ProcessOrderAsync(order);

                context.Logger.LogInformation(
                    "Successfully processed order: {OrderId}", order.OrderId);
            }
            catch (Exception ex)
            {
                // НЕ перекидаємо виключення — додаємо до списку невдалих
                context.Logger.LogError(ex,
                    "Failed to process message {MessageId}", message.MessageId);

                // Це повідомлення повернеться у SQS для повторної обробки
                batchItemFailures.Add(new SQSBatchResponse.BatchItemFailure
                {
                    ItemIdentifier = message.MessageId
                });
            }
        }

        return new SQSBatchResponse { BatchItemFailures = batchItemFailures };
    }
}

Тригер EventBridge: планові та подієво-орієнтовані задачі

Amazon EventBridge (раніше CloudWatch Events) — це serverless шина подій, що дозволяє маршрутизувати події між AWS-сервісами та власними застосунками. З точки зору Lambda, EventBridge надає два ключових патерни:

  1. Scheduled Events (Cron) — виклик Lambda за розкладом: щохвилини, щогодини, за cron-виразом
  2. Event Pattern Rules — виклик Lambda при настанні певної події в AWS-акаунті (наприклад, новий EC2 instance, зміна стану RDS, Custom Events від власних мікросервісів)
ScheduledCleanupFunction.cs
using Amazon.Lambda.Core;
using Amazon.Lambda.CloudWatchEvents.ScheduledEvents;

namespace Scheduler.Lambda;

public class ScheduledCleanupFunction
{
    private readonly ICleanupService _cleanupService;

    public ScheduledCleanupFunction(ICleanupService cleanupService)
    {
        _cleanupService = cleanupService;
    }

    // ScheduledEvent — типізована обгортка для EventBridge Scheduled Events
    public async Task FunctionHandler(
        ScheduledEvent scheduledEvent,
        ILambdaContext context)
    {
        context.Logger.LogInformation(
            "Scheduled cleanup started. Time={Time}, Source={Source}",
            scheduledEvent.Time, scheduledEvent.Source);

        // Перевіряємо достатність часу до Timeout
        // Для завдань cleanup це критично
        if (context.RemainingTime < TimeSpan.FromMinutes(2))
        {
            context.Logger.LogWarning(
                "Insufficient time remaining ({Remaining}), skipping cleanup",
                context.RemainingTime);
            return;
        }

        var result = await _cleanupService.CleanupExpiredSessionsAsync(
            olderThan: TimeSpan.FromDays(30),
            cancellationToken: CreateCancellationToken(context));

        context.Logger.LogInformation(
            "Cleanup complete. Deleted={Count} sessions", result.DeletedCount);
    }

    private static CancellationToken CreateCancellationToken(ILambdaContext context)
    {
        // Автоматично скасовуємо операцію за 30 секунд до Timeout
        var cts = new CancellationTokenSource(
            context.RemainingTime - TimeSpan.FromSeconds(30));
        return cts.Token;
    }
}

Environment Variables та Secrets Management

Environment Variables: зовнішня конфігурація функції

Environment Variables у Lambda виконують ту саму роль, що і у традиційних серверних застосунках: вони забезпечують механізм зовнішньої конфігурації без необхідності змінювати код функції. Lambda дозволяє визначити довільний набір пар ключ-значення, що стають доступними у коді через стандартний Environment.GetEnvironmentVariable().

Ключові характеристики Environment Variables у Lambda:

  • Зберігаються як частина конфігурації функції та автоматично присутні в кожному Execution Environment
  • Значення шифруються AWS KMS (за замовчуванням — AWS Managed Key, можна вказати власний KMS ключ)
  • Максимальний сумарний розмір усіх змінних — 4 KB
  • Змінюються без необхідності нового деплойменту коду (але потребують нового Execution Environment — тобто спричиняють Cold Start)

Доступ до Environment Variables з .NET:

Configuration.cs
namespace MyApi.Lambda;

// Рекомендована практика: типізований клас конфігурації
// замість прямих викликів Environment.GetEnvironmentVariable скрізь у коді
public sealed class LambdaConfiguration
{
    // Назва таблиці DynamoDB — змінюється між mid/prod оточеннями
    public string UsersTableName { get; } =
        Environment.GetEnvironmentVariable("USERS_TABLE_NAME")
        ?? throw new InvalidOperationException(
            "USERS_TABLE_NAME environment variable is not set");

    // URL зовнішнього API — різний для staging та production
    public string PaymentApiBaseUrl { get; } =
        Environment.GetEnvironmentVariable("PAYMENT_API_BASE_URL")
        ?? "https://api.payment.example.com";

    // Рівень логування
    public string LogLevel { get; } =
        Environment.GetEnvironmentVariable("LOG_LEVEL") ?? "Information";

    // Feature flag
    public bool EnableDetailedLogging { get; } =
        bool.TryParse(
            Environment.GetEnvironmentVariable("ENABLE_DETAILED_LOGGING"),
            out var value) && value;
}
Чутливі дані у Environment Variables. Хоча AWS шифрує значення Environment Variables «у спокої» (at rest), вони відображаються у відкритому вигляді у консолі AWS та через AWS CLI для будь-кого з правами lambda:GetFunctionConfiguration. Паролі, API ключі та токени не слід зберігати у Environment Variables у відкритому вигляді. Для секретів використовуйте AWS Secrets Manager або AWS Parameter Store (SSM).

AWS Secrets Manager: безпечне зберігання секретів

AWS Secrets Manager — це спеціалізований сервіс для зберігання, ротації та надання доступу до секретних значень: паролів баз даних, API ключів, OAuth токенів. На відміну від Environment Variables, Secrets Manager:

  • Підтримує автоматичну ротацію секрету (наприклад, щомісячне автоматичне оновлення пароля RDS)
  • Зберігає версії секрету та дозволяє атомарне оновлення без простою
  • Надає аудит доступу через CloudTrail
  • Інтегрується з IAM для гранулярного контролю доступу до конкретних секретів
Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

title "Lambda + Secrets Manager: безпечне отримання секретів"

participant "Lambda\n(Cold Start)" as LAM #d1fae5
participant "AWS Secrets Manager" as SM #fef3c7
participant "AWS KMS\n(шифрування)" as KMS #dbeafe
participant "Amazon RDS\nPostgreSQL" as RDS #f3e8ff

note over LAM : Init фаза (Cold Start)

LAM -> SM : GetSecretValue\n("prod/myapp/db-password")
SM -> KMS : Decrypt(encrypted_secret)
KMS --> SM : Розшифроване значення
SM --> LAM : {"username":"app_user",\n"password":"S3cr3t!Passw0rd",\n"host":"db.cluster...rds.amazonaws.com",\n"port":5432}

LAM -> LAM : Створити connection string\nПідключитися до RDS\n(зберегти підключення як static)

note over LAM : Warm Start (наступні виклики)
LAM -> RDS : Використовує збережене підключення\n(Secrets Manager НЕ викликається повторно!)

note right of LAM
  ВАЖЛИВО: кешуємо секрет у пам'яті!
  Виклик Secrets Manager при кожному
  запиті = +10-50ms latency та
  додаткові витрати (~$0.05/10K calls)
end note

@enduml

Реалізація безпечного отримання секрету з кешуванням у .NET:

SecretsManager.cs
using Amazon.SecretsManager;
using Amazon.SecretsManager.Model;
using System.Text.Json;

namespace MyApi.Lambda.Infrastructure;

public sealed class SecretsManagerService : ISecretsManagerService
{
    private readonly IAmazonSecretsManager _client;

    // Кеш секретів — статичний, живе протягом усього Execution Environment
    // Ключ: ARN секрету, Значення: (розшифроване значення, час кешування)
    private static readonly Dictionary<string, (string Value, DateTime CachedAt)>
        _secretsCache = new();

    // Час жизні кешу — 5 хвилин
    // Довше = менше витрат, але затримка оновлення при ротації
    private static readonly TimeSpan CacheTtl = TimeSpan.FromMinutes(5);

    public SecretsManagerService(IAmazonSecretsManager client)
    {
        _client = client;
    }

    public async Task<T> GetSecretAsync<T>(string secretArn) where T : class
    {
        // Перевіряємо кеш
        if (_secretsCache.TryGetValue(secretArn, out var cached))
        {
            if (DateTime.UtcNow - cached.CachedAt < CacheTtl)
                return JsonSerializer.Deserialize<T>(cached.Value)!;
        }

        // Запит до Secrets Manager (лише при Cache Miss або протермінованому кеші)
        var request = new GetSecretValueRequest { SecretId = secretArn };
        var response = await _client.GetSecretValueAsync(request);
        var secretValue = response.SecretString;

        // Оновлюємо кеш
        _secretsCache[secretArn] = (secretValue, DateTime.UtcNow);

        return JsonSerializer.Deserialize<T>(secretValue)!;
    }
}

// Типізований record для секрету бази даних
public record DatabaseSecret(
    string Username,
    string Password,
    string Host,
    int Port,
    string DbName)
{
    public string ToConnectionString() =>
        $"Host={Host};Port={Port};Database={DbName};" +
        $"Username={Username};Password={Password};SSL Mode=Require;";
}

Інтеграція у Startup з отриманням секрету при Cold Start:

Startup.cs
[LambdaStartup]
public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddAWSService<IAmazonSecretsManager>();
        services.AddSingleton<ISecretsManagerService, SecretsManagerService>();

        // Синхронне отримання рядка підключення при ініціалізації DI Container
        // Виконується ОДНОРАЗОВО у Cold Start — не при кожному виклику
        services.AddSingleton<IDbConnectionFactory>(provider =>
        {
            var secretsManager = provider.GetRequiredService<ISecretsManagerService>();
            var secretArn = Environment.GetEnvironmentVariable("DB_SECRET_ARN")!;

            // GetAwaiter().GetResult() допустимий у Cold Start
            // (не в handler — там завжди async!)
            var dbSecret = secretsManager
                .GetSecretAsync<DatabaseSecret>(secretArn)
                .GetAwaiter().GetResult();

            return new NpgsqlConnectionFactory(dbSecret.ToConnectionString());
        });
    }
}

Lambda Destinations: маршрутизація результатів асинхронних викликів

Lambda Destinations — це конфігурація, що визначає, куди Lambda-сервіс автоматично надсилає результат асинхронного виклику залежно від того, завершилась функція успіхом чи помилкою. Destinations усувають необхідність вручну реалізовувати логіку маршрутизації результатів безпосередньо в коді функції.

Lambda Destinations працюють виключно для асинхронних викликів (S3, SNS, EventBridge, прямий асинхронний виклик SDK) та poll-based тригерів (SQS, DynamoDB Streams). Для синхронних тригерів (API Gateway) Destinations недоступні — відповідь повертається напряму клієнту.
Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

title "Lambda Destinations: маршрутизація результатів"

rectangle "S3 Event\n(Trigger)" as S3T #fef3c7
rectangle "Lambda Function\nFileProcessor" as LAM #d1fae5

rectangle "On Success Destination" as OSD #d1fae5 {
    rectangle "EventBridge\n(для оркестрації наступного кроку)" as EB #dbeafe
    rectangle "SQS Queue\n(для подальшої обробки)" as SQSSUC #bbf7d0
    rectangle "SNS Topic\n(для нотифікацій)" as SNSSUC #bbf7d0
    rectangle "Інша Lambda\n(ланцюжок функцій)" as LAMDST #bbf7d0
}

rectangle "On Failure Destination" as OFD #fee2e2 {
    rectangle "SQS Dead Letter Queue\n(для аналізу невдалих подій)" as DLQ #fca5a5
    rectangle "SNS Topic\n(алерт команді DevOps)" as SNSFAIL #fca5a5
    rectangle "EventBridge\n(для автоматичного rollback)" as EBFAIL #fca5a5
}

S3T -down-> LAM : Асинхронний виклик

LAM -right-> OSD : SUCCESS\n(функція завершилась без виключення)
LAM -left-> OFD : FAILURE\n(вичерпано retry attempts\nабо невідновлювана помилка)

@enduml

Що Lambda автоматично надсилає у Destination:

Lambda формує збагачений JSON-payload, що містить повний контекст виклику — не лише результат, але й метадані про саму функцію та оригінальну подію. Це усуває необхідність ручного збору контексту у коді.

{
    "version": "1.0",
    "timestamp": "2024-03-15T10:30:00.123Z",
    "requestContext": {
        "requestId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
        "functionArn": "arn:aws:lambda:eu-central-1:123456789012:function:FileProcessor",
        "condition": "Success",
        "approximateInvokeCount": 1
    },
    "requestPayload": {
        "Records": [{ "eventSource": "aws:s3", "s3": { "object": { "key": "report.csv" } } }]
    },
    "responseContext": {
        "statusCode": 200,
        "executedVersion": "$LATEST"
    },
    "responsePayload": {
        "processedRows": 15420,
        "outputKey": "processed/report-2024-03-15.parquet"
    }
}

Порівняння з Dead Letter Queue (DLQ):

ХарактеристикаLambda DestinationsDead Letter Queue
Де спрацьовуєOn Success та On FailureЛише On Failure
Що передаєтьсяПовний контекст: input + output + metadataЛише оригінальний input event
Підтримувані ціліSQS, SNS, EventBridge, LambdaSQS, SNS
Коли з'явилось2019 (новіший підхід)2016 (legacy підхід)
Рекомендація✅ Використовуйте для нових проектів⚠️ Тільки для зворотної сумісності


Lambda Performance Optimization: стратегії мінімізації Cold Start

Оптимізація продуктивності Lambda для .NET є комплексним завданням, що охоплює декілька незалежних рівнів: архітектуру коду, конфігурацію Runtime, розмір deployment package та апаратні ресурси. Розглянемо кожен рівень детально.

Стратегія 1: оптимізація коду Init-фази

Найефективніший підхід — мінімізувати обсяг роботи, що виконується поза handler-функцією (тобто у Init-фазі Cold Start). Кожен об'єкт, ініціалізований у Init-фазі, живе протягом усього Execution Environment і використовується повторно при Warm Start. Це є перевагою, але Init-фаза повинна ініціалізувати лише те, що справді потрібно.

Оптимальна структура ініціалізації
// ❌ ПОГАНО: Lazy ініціалізація у handler — Cold Start при КОЖНОМУ виклику,
// якщо об'єкт ще не ініціалізований
public class BadFunction
{
    public async Task Handler(S3Event evt, ILambdaContext ctx)
    {
        // Новий клієнт створюється при першому виклику кожного Execution Environment
        // Але також — при кожному Cold Start нового Execution Environment
        var s3Client = new AmazonS3Client(); // ❌
        var result = await s3Client.ListBucketsAsync();
    }
}

// ✅ ДОБРЕ: Статична ініціалізація — виконується ОДНОРАЗОВО при Cold Start
// Повторно використовується при всіх Warm Start викликах
public class GoodFunction
{
    // static readonly — ініціалізується при завантаженні класу (Init-фаза)
    private static readonly IAmazonS3 S3Client = new AmazonS3Client();
    private static readonly IAmazonDynamoDB DynamoClient = new AmazonDynamoDBClient();

    // HttpClient НІКОЛИ не створюється у handler — connection pooling!
    private static readonly HttpClient HttpClient = new HttpClient
    {
        BaseAddress = new Uri(Environment.GetEnvironmentVariable("API_BASE_URL")!),
        Timeout = TimeSpan.FromSeconds(10)
    };

    public async Task<string> Handler(APIGatewayHttpApiV2ProxyRequest req, ILambdaContext ctx)
    {
        // Handler лише використовує вже ініціалізовані об'єкти ✅
        var response = await HttpClient.GetStringAsync("/health");
        return response;
    }
}

Lazy initialization для важких залежностей:

Якщо певна залежність використовується не у кожному виклику (наприклад, лише при конкретному типі події), можна застосувати Lazy<T> для відкладеної ініціалізації — вона виконається при першому зверненні, але лише один раз протягом Execution Environment:

public class OptimizedFunction
{
    // Lazy ініціалізація — підключення до ElasticSearch виконується
    // лише при першому зверненні, але не затримує Init-фазу
    private static readonly Lazy<IElasticClient> ElasticClient = new(() =>
    {
        var settings = new ConnectionSettings(
            new Uri(Environment.GetEnvironmentVariable("ELASTICSEARCH_URL")!));
        return new ElasticClient(settings);
    });

    public async Task Handler(SQSEvent evt, ILambdaContext ctx)
    {
        // Lazy.Value ініціалізує клієнт лише один раз
        var client = ElasticClient.Value;
        // ...
    }
}

Стратегія 2: вибір оптимального обсягу пам'яті

Ця стратегія є одним із найпоширеніших нерозуміних аспектів оптимізації Lambda. Більше пам'яті = більше CPU. Збільшення пам'яті часто знижує загальну вартість, оскільки функція виконується швидше і загальна сума GB-секунд зменшується попри вищу ставку за GB.

Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

title "Залежність між пам'яттю, часом виконання та вартістю"

rectangle "128 MB\n(мінімум)" as M128 #fca5a5 {
    rectangle "CPU: 1/14 vCPU\nЧас: 3200ms\nGB-сек: 0.41\nВартість: $0.0000068" as V128
}

rectangle "512 MB" as M512 #fde68a {
    rectangle "CPU: 1/3.5 vCPU\nЧас: 1200ms\nGB-сек: 0.61\nВартість: $0.0000102" as V512
}

rectangle "1769 MB\n(1 повний vCPU)" as M1769 #bbf7d0 {
    rectangle "CPU: 1 vCPU\nЧас: 500ms\nGB-сек: 0.88\nВартість: $0.0000147" as V1769
}

rectangle "3008 MB" as M3008 #dbeafe {
    rectangle "CPU: ~1.7 vCPU\nЧас: 280ms\nGB-sek: 0.84\nВартість: $0.0000139" as V3008
}

note bottom of M128 : Найдешевша ставка,\nАЛЕ найвища загальна вартість!\nCold Start: ~3s
note bottom of M512 : Баланс для легких функцій
note bottom of M1769 : Оптимум для більшості .NET функцій\nCold Start: <1s
note bottom of M3008 : Для CPU-інтенсивних задач

@enduml
AWS Lambda Power Tuning — відкрита AWS Step Functions State Machine, що автоматично тестує вашу функцію з різними обсягами пам'яті та знаходить оптимальну точку між швидкістю та вартістю. Запустіть її перед виведенням функції у production: github.com/alexcasalboni/aws-lambda-power-tuning.

Стратегія 3: оптимізація розміру deployment package

Розмір ZIP-архіву безпосередньо впливає на тривалість Cold Start: Lambda завантажує весь архів при ініціалізації нового Execution Environment. Для .NET проектів основний внесок у розмір вносять NuGet-залежності та нативні бінарники.

Техніки зменшення розміру для .NET:

MyFunction.csproj
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>

    <!-- Публікуємо self-contained з trimming для видалення невикористаних типів -->
    <PublishReadyToRun>true</PublishReadyToRun>

    <!-- Видаляє невикористані типи та члени при публікації -->
    <!-- Зменшує розмір на 30-60% для типових .NET Lambda -->
    <PublishTrimmed>true</PublishTrimmed>

    <!-- Генерує нативний код заздалегідь → швидший запуск CLR -->
    <TieredPGO>true</TieredPGO>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Amazon.Lambda.Core" Version="2.2.0" />
    <PackageReference Include="Amazon.Lambda.Serialization.SystemTextJson" Version="2.4.1" />
    <!-- Використовуємо System.Text.Json замість Newtonsoft.Json — менший розмір! -->

    <!-- Для мінімізації: включаємо лише потрібні AWSSDK пакети -->
    <!-- ❌ Не: AWSSDK.Core (тягне ВСЕ) -->
    <!-- ✅ Так: точечні пакети для конкретних сервісів -->
    <PackageReference Include="AWSSDK.S3" Version="3.7.*" />
    <PackageReference Include="AWSSDK.DynamoDBv2" Version="3.7.*" />
  </ItemGroup>
</Project>

Команда публікації з оптимізацією для Lambda:

# Публікація з trimming та ReadyToRun для зменшення Cold Start
dotnet publish src/MyFunction \
    --configuration Release \
    --runtime linux-x64 \
    --self-contained true \
    --output ./publish \
    -p:PublishTrimmed=true \
    -p:PublishReadyToRun=true

# Пакуємо у ZIP для завантаження у Lambda
cd publish && zip -r ../function.zip .

# Розмір до та після оптимізації (типові результати):
# До: 48 MB | Після trimming: 18 MB | Зменшення: 62%

Стратегія 4: .NET Native AOT — максимальна мінімізація Cold Start

Native AOT (Ahead-of-Time) компіляція — це технологія .NET, що компілює код безпосередньо у нативний машинний код Linux заздалегідь, повністю усуваючи необхідність запуску CLR та JIT-компіляції при ініціалізації. Результатом є самодостатній бінарний файл, що запускається на порядок швидше за стандартний .NET Runtime.

Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

title ".NET стандартний Runtime vs Native AOT: Cold Start"

rectangle "Стандартний .NET 10 Runtime" as STD #fef3c7 {
    rectangle "1. Завантаження CLR\n(Common Language Runtime)" as CLR #fca5a5
    rectangle "2. Завантаження Assembly (.dll)" as ASM #fca5a5
    rectangle "3. JIT-компіляція методів\n(Just-in-Time, при першому виклику)" as JIT #fca5a5
    rectangle "4. Init-код (DI, static ctor)" as INIT #fde68a
    rectangle "5. Виконання Handler" as HANDLER1 #bbf7d0
}

rectangle "Native AOT (.NET 10)" as AOT #d1fae5 {
    rectangle "1. Завантаження бінарника\n(нативний Linux ELF, ~10-15MB)" as BIN #bbf7d0
    rectangle "2. Init-код (DI, static ctor)" as INIT2 #fde68a
    rectangle "3. Виконання Handler" as HANDLER2 #bbf7d0
}

note right of STD
  Cold Start: ~800-3000ms
  Розмір: 30-100MB (ZIP)
  Сумісність: 100% NuGet
end note

note right of AOT
  Cold Start: ~50-150ms (10-20x швидше!)
  Розмір: 8-20MB (ZIP)
  Обмеження: немає рефлексії, Roslyn,
  dynamic loading. Не всі NuGet сумісні.
end note

@enduml

Налаштування проекту для Native AOT Lambda:

NativeAotFunction.csproj
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>

    <!-- Увімкнення Native AOT -->
    <PublishAot>true</PublishAot>

    <!-- Для AOT необхідна явна вказівка типів серіалізації -->
    <!-- (рефлексія недоступна в AOT для JSON серіалізації) -->
  </PropertyGroup>

  <ItemGroup>
    <!-- Спеціальний пакет для Native AOT Lambda -->
    <PackageReference Include="Amazon.Lambda.RuntimeSupport" Version="1.10.*" />
    <PackageReference Include="Amazon.Lambda.Core" Version="2.2.0" />
    <PackageReference Include="Amazon.Lambda.Serialization.SystemTextJson" Version="2.4.1" />
  </ItemGroup>
</Project>

Native AOT потребує явної реєстрації типів для JSON серіалізації через Source Generators:

NativeAotFunction.cs
using Amazon.Lambda.Core;
using Amazon.Lambda.RuntimeSupport;
using Amazon.Lambda.Serialization.SystemTextJson;
using System.Text.Json.Serialization;

// Source Generator генерує AOT-сумісний серіалізатор для вказаних типів
[JsonSerializable(typeof(ApiRequest))]
[JsonSerializable(typeof(ApiResponse))]
[JsonSerializable(typeof(Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyRequest))]
[JsonSerializable(typeof(Amazon.Lambda.APIGatewayEvents.APIGatewayHttpApiV2ProxyResponse))]
public partial class LambdaJsonContext : JsonSerializerContext { }

// Точка входу для Native AOT Lambda
static class Program
{
    static async Task Main(string[] args)
    {
        // Зовнішнє налаштування — без DI контейнера (AOT-сумісно)
        var handler = new OrderHandler();

        // Bootstrap Lambda Runtime з AOT-сумісним серіалізатором
        var serializer = new SourceGeneratorLambdaJsonSerializer<LambdaJsonContext>();

        await LambdaBootstrapBuilder
            .Create<ApiRequest, ApiResponse>(
                handler.HandleAsync,
                serializer)
            .Build()
            .RunAsync();
    }
}

public sealed class OrderHandler
{
    // Статичні клієнти — ініціалізуються при запуску процесу
    private static readonly IAmazonDynamoDB _dynamo = new AmazonDynamoDBClient();

    public async Task<ApiResponse> HandleAsync(
        ApiRequest request, ILambdaContext context)
    {
        context.Logger.LogInformation(
            "AOT Handler invoked: {RequestId}", context.AwsRequestId);

        // Бізнес-логіка...
        return new ApiResponse { StatusCode = 200, Body = "OK" };
    }
}

public record ApiRequest(string Path, string Method, string? Body);
public record ApiResponse(int StatusCode, string Body);
Обмеження Native AOT. Native AOT несумісний з динамічними можливостями .NET: рефлексія (Reflection), dynamic, Assembly.Load, Activator.CreateInstance без Source Generators. Деякі популярні бібліотеки (AutoMapper, Entity Framework Core) не сумісні з AOT або мають обмежену підтримку. Перевіряйте сумісність всіх NuGet-залежностей перед переходом на AOT.

Provisioned Concurrency: усунення Cold Start для критичних функцій

Provisioned Concurrency — це механізм, що дозволяє AWS підтримувати вказану кількість заздалегідь ініціалізованих Execution Environments у постійній готовності. На відміну від звичайних (on-demand) Execution Environments, що ініціалізуються при надходженні запиту (Cold Start), Provisioned Environments вже прогріті та готові обробляти виклики негайно.

Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

title "On-Demand vs Provisioned Concurrency"

rectangle "On-Demand Concurrency\n(стандартна поведінка)" as OD #fef3c7 {
    rectangle "Виклик 1 → Cold Start\n(Init: ~1200ms + Handler: ~50ms)\nЗАГАЛОМ: ~1250ms" as OD1 #fca5a5
    rectangle "Виклик 2 (warm)\n(Handler: ~50ms)\nЗАГАЛОМ: ~50ms" as OD2 #bbf7d0
    rectangle "Новий Execution Env → Cold Start\n(Init: ~1200ms + Handler: ~50ms)\nЗАГАЛОМ: ~1250ms" as OD3 #fca5a5
}

rectangle "Provisioned Concurrency = 5\n(5 Execution Envs заздалегідь ініціалізовані)" as PC #d1fae5 {
    rectangle "Виклик 1\n(Handler: ~50ms)\n← НЕМАЄ Cold Start!" as PC1 #bbf7d0
    rectangle "Виклик 2\n(Handler: ~50ms)\n← НЕМАЄ Cold Start!" as PC2 #bbf7d0
    rectangle "Виклик 5\n(Handler: ~50ms)\n← НЕМАЄ Cold Start!" as PC3 #bbf7d0
    rectangle "Виклик 6+ → On-Demand\n(Cold Start якщо PC вичерпано)" as PC4 #fde68a
}

note bottom of OD : Безкоштовно, але непередбачувана латентність
note bottom of PC : Додаткова вартість: ~$0.015/год за 1 env\nАЛЕ: гарантована латентність <100ms

@enduml

Коли Provisioned Concurrency є необхідністю

Не кожна Lambda-функція потребує Provisioned Concurrency. Цей механізм є виправданим лише в конкретних сценаріях:

User-facing API

API Gateway + Lambda, де Cold Start безпосередньо впливає на користувацький досвід. P99 latency > 2 секунди є неприйнятним для інтерактивних застосунків.

Рекомендація: PC = мінімальна очікувана одночасна кількість запитів у пік

SLA-критичні функції

Функції з жорсткими SLA (<500ms response time), де Cold Start є порушенням контракту. Наприклад: платіжні шлюзи, системи real-time торгів.

Функції з дорогим Init

ML-моделі, що завантажуються у Init-фазі (100-500 MB модель), або функції зі складними DI-контейнерами (> 3 секунд ініціалізації).

Конфігурація Provisioned Concurrency

Налаштування через AWS CLI:

# Крок 1: опублікувати нову версію функції
# (Provisioned Concurrency застосовується до версій, не до $LATEST)
aws lambda publish-version \
    --function-name dotnet-api-function \
    --description "v1.0.0 — production release" \
    --region eu-central-1

# Крок 2: зберегти ARN версії
FUNCTION_VERSION_ARN=$(aws lambda list-versions-by-function \
    --function-name dotnet-api-function \
    --region eu-central-1 \
    --query "Versions[-1].FunctionArn" \
    --output text)

echo "Function version: $FUNCTION_VERSION_ARN"

# Крок 3: увімкнути Provisioned Concurrency для цієї версії
aws lambda put-provisioned-concurrency-config \
    --function-name dotnet-api-function \
    --qualifier 1 \
    --provisioned-concurrent-executions 5 \
    --region eu-central-1
put-provisioned-concurrency-config
$ aws lambda put-provisioned-concurrency-config --function-name dotnet-api-function --qualifier 1 --provisioned-concurrent-executions 5
{
"RequestedProvisionedConcurrentExecutions": 5,
"AvailableProvisionedConcurrentExecutions": 0,
"AllocatedProvisionedConcurrentExecutions": 0,
"Status": "IN_PROGRESS",
"StatusReason": "Provisioning 5 concurrent executions",
"LastModified": "2024-03-15T10:30:00+0000"
}
← Статус "IN_PROGRESS": AWS ініціалізує 5 Execution Environments (~2-3 хвилини)

Перевірка готовності Provisioned Concurrency:

# Очікуємо переходу статусу з IN_PROGRESS на READY
aws lambda get-provisioned-concurrency-config \
    --function-name dotnet-api-function \
    --qualifier 1 \
    --region eu-central-1
Статус READY — Provisioned Concurrency активний
$ aws lambda get-provisioned-concurrency-config --function-name dotnet-api-function --qualifier 1
{
"RequestedProvisionedConcurrentExecutions": 5,
"AvailableProvisionedConcurrentExecutions": 5,
"AllocatedProvisionedConcurrentExecutions": 5,
"Status": "READY",
"LastModified": "2024-03-15T10:32:47+0000"
}
← READY: всі 5 Execution Environments ініціалізовані та чекають викликів

Application Auto Scaling для Provisioned Concurrency

Оскільки Provisioned Concurrency має фіксовану щогодинну вартість, утримувати максимальний рівень цілодобово є нераціональним. Application Auto Scaling дозволяє автоматично змінювати кількість Provisioned Environments за розкладом або на основі метрик.

# Реєстрація Lambda як scalable target для Application Auto Scaling
aws application-autoscaling register-scalable-target \
    --service-namespace lambda \
    --resource-id "function:dotnet-api-function:1" \
    --scalable-dimension "lambda:function:ProvisionedConcurrency" \
    --min-capacity 2 \
    --max-capacity 50 \
    --region eu-central-1

# Створення Scheduled Action: збільшення PC о 07:00 UTC (ранкова активність)
aws application-autoscaling put-scheduled-action \
    --service-namespace lambda \
    --resource-id "function:dotnet-api-function:1" \
    --scalable-dimension "lambda:function:ProvisionedConcurrency" \
    --scheduled-action-name "scale-up-morning" \
    --schedule "cron(0 7 * * ? *)" \
    --scalable-target-action MinCapacity=10,MaxCapacity=50 \
    --region eu-central-1

# Зменшення PC о 22:00 UTC (нічний мінімум)
aws application-autoscaling put-scheduled-action \
    --service-namespace lambda \
    --resource-id "function:dotnet-api-function:1" \
    --scalable-dimension "lambda:function:ProvisionedConcurrency" \
    --scheduled-action-name "scale-down-night" \
    --schedule "cron(0 22 * * ? *)" \
    --scalable-target-action MinCapacity=2,MaxCapacity=50 \
    --region eu-central-1

Моніторинг Lambda через Amazon CloudWatch

Lambda автоматично публікує набір метрик у CloudWatch без будь-якого налаштування. Розуміння цих метрик є фундаментальним для операційного контролю над serverless системою.

Invocations

Кількість викликів функції за період. Включає успішні та невдалі виклики. Базова метрика для розуміння навантаження та оцінки витрат.

Errors

Кількість викликів, що завершились помилкою: необроблені виключення, таймаути, помилки MemoryExceeded. Error Rate = Errors / Invocations.

Duration

Час виконання у мілісекундах: Average, P50, P95, P99. P99 Duration є критичним показником: він відображає найгірший досвід 1% користувачів.

Throttles

Кількість викликів, що відхилені через перевищення Concurrency Limit. Якщо Throttles > 0 — функція досягла ліміту паралелізму і деякі запити не обробляються.

ConcurrentExecutions

Поточна кількість одночасно виконуваних інстанцій. Корисна для відстеження наближення до Concurrency Limit (1,000 за замовчуванням).

InitDuration

Час Cold Start Init-фази у мілісекундах. Відображається лише для викликів, що викликали Cold Start. Ключова метрика для відстеження ефективності оптимізацій.

Рекомендований CloudWatch Dashboard для Lambda:

# Створення CloudWatch Alarm для відстеження критичного рівня помилок
aws cloudwatch put-metric-alarm \
    --alarm-name "lambda-dotnet-error-rate-critical" \
    --alarm-description "Error rate > 5% for 2 consecutive periods" \
    --metric-name Errors \
    --namespace AWS/Lambda \
    --dimensions Name=FunctionName,Value=dotnet-api-function \
    --statistic Sum \
    --period 60 \
    --evaluation-periods 2 \
    --threshold 5 \
    --comparison-operator GreaterThanOrEqualToThreshold \
    --treat-missing-data notBreaching \
    --alarm-actions "arn:aws:sns:eu-central-1:123456789012:ops-alerts" \
    --region eu-central-1

# Alarm для відстеження Cold Start тривалості > 3 секунд
aws cloudwatch put-metric-alarm \
    --alarm-name "lambda-cold-start-too-long" \
    --alarm-description "Init duration > 3000ms — investigate optimization" \
    --metric-name InitDuration \
    --namespace AWS/Lambda \
    --dimensions Name=FunctionName,Value=dotnet-api-function \
    --statistic p99 \
    --period 300 \
    --evaluation-periods 1 \
    --threshold 3000 \
    --comparison-operator GreaterThanThreshold \
    --alarm-actions "arn:aws:sns:eu-central-1:123456789012:ops-alerts" \
    --region eu-central-1


Mini-Workshop: Перший деплой Lambda-функції

У цьому міні-воркшопі ми пройдемо шлях від встановлення інструментів до запуску вашої першої функції в хмарі. Ми розглянемо два підходи: спрощений (автоматизований) та повний (через S3 та AWS CLI).

1. Підготовка інструментарію

Для роботи з .NET Lambda в AWS нам знадобиться спеціальний інструмент командного рядка, який розширює стандартний dotnet cli.

# 0. Встановлюємо шаблони
dotnet new install Amazon.Lambda.Templates

# 1. Встановлюємо глобальний інструмент Amazon.Lambda.Tools
dotnet tool install -g Amazon.Lambda.Tools

# 2. Оновлюємо (якщо вже був встановлений)
dotnet tool update -g Amazon.Lambda.Tools

# 3. Перевіряємо готовність
dotnet lambda --version

2. Створення тестового проекту

Створимо найпростішу функцію "Hello World", щоб перевірити ланцюжок деплойменту.

# Створюємо проект за допомогою AWS шаблону
dotnet new lambda.EmptyFunction -n MyFirstLambda -o src/MyFirstLambda
cd src/MyFirstLambda

# Додаємо залежності та перевіряємо проект
dotnet restore

3. Автоматизований деплой (dotnet lambda)

Це найшвидший спосіб для розробки та тестування. Команда deploy-function бере на себе компіляцію, пакування, завантаження та оновлення конфігурації.

# Деплой з автоматичним створенням/оновленням функції
dotnet lambda deploy-function MyFirstLambda \
    --runtime dotnet10 \
    --handler MyFirstLambda::MyFirstLambda.Function::FunctionHandler \
    --memory-size 256 \
    --timeout 30 \
    --region eu-central-1
При першому деплої інструмент запитає вас, яку IAM Role призначити функції. Ви можете обрати існуючу або дозволити створити нову з базовими правами на логування (AWSLambdaBasicExecutionRole).

4. Повний контроль: Деплой через S3 та AWS CLI

У великих проектах та CI/CD пайплайнах часто використовують двостадійний деплой: спочатку завантажують пакет у S3, а потім оновлюють функцію. Це надійніше для великих артефактів (> 50MB).

Крок A: Пакування артефакту

dotnet lambda package \
    --configuration Release \
    --output-package ./publish/function.zip

Крок B: Завантаження в S3

aws s3 cp ./publish/function.zip \
    s3://my-deployment-bucket/v1/my-first-lambda.zip \
    --region eu-central-1

Крок C: Оновлення коду Lambda

aws lambda update-function-code \
    --function-name MyFirstLambda \
    --s3-bucket my-deployment-bucket \
    --s3-key v1/my-first-lambda.zip \
    --region eu-central-1

Крок D: Оновлення конфігурації (опційно)

aws lambda update-function-configuration \
    --function-name MyFirstLambda \
    --memory-size 512 \
    --environment "Variables={ENV=production,DEBUG=false}" \
    --region eu-central-1

5. Виклик функції та перевірка результату

Після успішного деплою ми можемо викликати нашу функцію і перевірити, чи вона працює коректно. Оскільки ми очікуємо рядок і повертаємо його у верхньому регістрі, передамо тестовий рядок.

# Викликаємо функцію з тестовим навантаженням (payload)
dotnet lambda invoke-function MyFirstLambda \
    --payload '"hello serverless world"' \
    --region eu-central-1

6. Активація публічного лінка (Lambda Function URL)

Оскільки ми розглядаємо автоматизацію та повний контроль, найшвидше це зробити через AWS CLI.

Крок A: Створення конфігурації URL

Вмикаємо функцію URL для нашої Lambda. Прапор --auth-type NONE означає, що лінк буде публічним і не вимагатиме підписів AWS IAM для кожного запиту.

aws lambda create-function-url-config \
    --function-name MyFirstLambda \
    --auth-type NONE \
    --region eu-central-1

Крок B: Дозвіл на публічний доступ (CORS / Permissions)

За замовчуванням AWS заблокує будь-які анонімні запити з інтернету з міркувань безпеки. Нам потрібно явно дозволити всьому світу (*) викликати цей URL:

aws lambda add-permission \
    --function-name MyFirstLambda \
    --statement-id FunctionURLAllowPublicAccess \
    --action lambda:InvokeFunctionUrl \
    --principal "*" \
    --function-url-auth-type NONE \
    --region eu-central-1

Крок C: Отримання вашого готового лінка

Тепер дізнаємося згенеровану адресу нашої функції:

aws lambda get-function-url-config \
    --function-name MyFirstLambda \
    --region eu-central-1 \
    --query FunctionUrl \
    --output text

Як це протестувати?

У результаті виконання останньої команди ви отримаєте унікальний лінк, який виглядає приблизно так: https://abcdefg1234567890.lambda-url.eu-central-1.on.aws/.

Оскільки ваш метод FunctionHandler у C# приймає рядок і повертає його в UPPERCASE, ви можете протестувати його через звичайний curl у терміналі (або через Postman), передавши текст у тілі POST-запиту:

curl -X POST https://your-lambda-url.on.aws/ \
    -d '"hello from internet"' \
    -H "Content-Type: application/json"
Важливий нюанс для .NET: Оскільки функція очікує чистий JSON-рядок, у утиліті curl ми передаємо текст у подвійних лапках всередині одинарних ('"hello"'), щоб серіалізатор SystemTextJson правильно його розпізнав. Результатом у консолі буде: "HELLO FROM INTERNET".

7. Інтеграція з Amazon API Gateway (Альтернатива Function URL)

Якщо вам потрібен більший контроль над маршрутизацією, авторизацією (наприклад, інтеграція з Cognito), лімітами (Rate Limiting) або ви хочете побудувати повноцінний REST API, замість Function URL краще використовувати Amazon API Gateway.

Найпростіший спосіб створити HTTP API та підключити його до нашої Lambda-функції через AWS CLI:

Крок A: Створення HTTP API

aws apigatewayv2 create-api \
    --name MyFirstLambdaAPI \
    --protocol-type HTTP \
    --target arn:aws:lambda:eu-central-1:ACCOUNT_ID:function:MyFirstLambda \
    --region eu-central-1

Зверніть увагу: замініть ACCOUNT_ID на ваш реальний ідентифікатор аккаунта.

Крок B: Надання дозволу API Gateway на виклик функції

Як і у випадку з Function URL, Lambda повинна явно дозволити API Gateway викликати себе:

aws lambda add-permission \
    --function-name MyFirstLambda \
    --statement-id ApiGatewayInvokeAccess \
    --action lambda:InvokeFunction \
    --principal apigateway.amazonaws.com \
    --region eu-central-1

Крок C: Отримання Endpoint URL

Після створення API (Крок A) у відповіді ви отримаєте поле ApiEndpoint. Ви також можете дізнатись його такою командою:

aws apigatewayv2 get-apis \
    --query "Items[?Name=='MyFirstLambdaAPI'].ApiEndpoint" \
    --output text \
    --region eu-central-1

Як це протестувати?

Використовуйте отриманий ApiEndpoint (наприклад, https://xyz123.execute-api.eu-central-1.amazonaws.com) аналогічно до Function URL:

curl -X POST https://your-api-gateway-url.amazonaws.com/ \
    -d '"hello via api gateway"' \
    -H "Content-Type: application/json"

8. Використання AWS Toolkit (IDE)

Якщо ви віддаєте перевагу роботі в IDE, AWS надає потужні розширення (Toolkit) для Visual Studio, JetBrains Rider та VS Code.

  • Visual Studio: Дозволяє створювати проекти з шаблонів, запускати локальне тестування (Mock Lambda Test Tool) та деплоїти правою кнопкою миші.
  • VS Code: Надає інтерфейс для перегляду логів CloudWatch прямо в редакторі, керування Lambda-функціями та локального дебагу через SAM CLI.
  • Rider: Найкраща підтримка для macOS/Linux розробників з глибокою інтеграцією AWS-ресурсів та шаблонів проектів.
Для локальної розробки та дебагу .NET Lambda без завантаження в хмару використовуйте Amazon.Lambda.TestTool. Це дозволяє імітувати вхідні події (JSON) та зупинятися на breakpoints у вашому коді.

Підсумок: коли використовувати Lambda, а коли — EC2/ECS

Вибір між Lambda та традиційними обчислювальними сервісами є архітектурним рішенням, що залежить від конкретних характеристик навантаження, вимог до латентності та операційної складності.

КритерійLambdaEC2 / ECS
Час запускуCold Start 100ms – 3sПостійно запущений (0ms latency)
Максимальний час виконання15 хвилинНеобмежено
Стан між запитамиStateless (ephemeral)Stateful (можливо)
МасштабуванняАвтоматичне, миттєвеASG (хвилини)
Вартість при низькому навантаженніМайже нульоваФіксована
Вартість при високому навантаженніЛінійна (може бути вищою)Фіксована + резерви
Операційна складністьМінімальнаСередня — висока
Контроль над середовищемОбмеженийПовний

Lambda є оптимальним вибором для:

  • Event-driven обробки: S3 файлів, SQS повідомлень, DynamoDB Streams
  • Нерегулярного трафіку з піками та тривалими паузами
  • Простих HTTP API з низьким та середнім навантаженням
  • Задач за розкладом (cron jobs)
  • Glue-коду між AWS-сервісами

EC2 або ECS є кращим вибором для:

  • Довготривалих задач (> 15 хвилин)
  • Систем з постійно-стабільним навантаженням (Lambda буде дорожчою)
  • Застосунків з жорсткими вимогами до Cold Start без бажання платити за Provisioned Concurrency
  • Систем, що потребують специфічного апаратного забезпечення (GPU, великий обсяг RAM)
  • Stateful додатків з WebSocket з'єднаннями тривалістю > 15 хвилин

Практичний воркшоп: Побудова системи обробки зображень (A-Z)

У цьому розділі ми побудуємо повноцінну serverless-систему «від А до Я». Наш сценарій: автоматичне створення мініатюр (thumbnails) для зображень, що завантажуються користувачами в S3.

Архітектура рішення

Система працює за подієво-орієнтованою моделлю:

  1. Користувач завантажує оригінальне зображення у source-images-bucket.
  2. S3 генерує подію s3:ObjectCreated:* та викликає Lambda-функцію.
  3. Lambda-функція (.NET 10) завантажує оригінал, змінює розмір та зберігає результат у processed-images-bucket.
  4. Всі дії логуються у CloudWatch для моніторингу.
Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

actor "Користувач" as USR
rectangle "S3: source-bucket" as S3_SRC #fef3c7
rectangle "AWS Lambda\n(ImageProcessor)" as LAM #d1fae5
rectangle "S3: processed-bucket" as S3_DST #dbeafe

USR -> S3_SRC : 1. Upload image.jpg
S3_SRC -> LAM : 2. Trigger Event
LAM -> S3_SRC : 3. GetObject
LAM -> LAM : 4. Resize Image
LAM -> S3_DST : 5. PutObject (thumbnail.jpg)
LAM -> "CloudWatch Logs" : 6. Logging
@enduml

Крок 1: Підготовка інфраструктури (S3 Buckets)

Перш за все, нам потрібно створити два S3-кошики: один для оригіналів, інший для мініатюр.

  1. Перейдіть у сервіс S3.
  2. Натисніть Create bucket.
  3. Bucket name: kostyl-dev-source-images-[unique-id].
  4. Region: eu-central-1.
  5. Залиште інші налаштування за замовчуванням та натисніть Create bucket.
  6. Повторіть кроки для створення другого кошика з іменем kostyl-dev-processed-images-[unique-id].

Крок 2: Налаштування безпеки (IAM Role)

Lambda-функція повинна мати права на читання з одного кошика та запис у інший.

  1. Перейдіть у сервіс IAMRolesCreate role.
  2. Trusted entity: AWS serviceLambda. Натисніть Next.
  3. Натисніть Create policy (відкриється нова вкладка).
  4. Оберіть вкладку JSON та вставте наступний код (замініть імена bucket на ваші):
    {
        "Version": "2012-10-17",
        "Statement": [
            {
                "Effect": "Allow",
                "Action": ["s3:GetObject"],
                "Resource": "arn:aws:s3:::kostyl-dev-source-images/*"
            },
            {
                "Effect": "Allow",
                "Action": ["s3:PutObject"],
                "Resource": "arn:aws:s3:::kostyl-dev-processed-images/*"
            },
            {
                "Effect": "Allow",
                "Action": ["logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents"],
                "Resource": "arn:aws:logs:*:*:*"
            }
        ]
    }
    
  5. Назвіть політику ImageProcessorPolicy та збережіть.
  6. Поверніться до вкладки створення ролі, оновіть список та оберіть вашу нову політику.
  7. Назвіть роль ImageProcessorRole та натисніть Create role.

Крок 3: Розробка .NET 10 коду

Ми використаємо бібліотеку SixLabors.ImageSharp для обробки зображень та офіційний AWS SDK.

# 1. Створюємо проект (класична бібліотека)
dotnet new classlib -n ImageProcessor.Lambda
cd ImageProcessor.Lambda

# 2. Додаємо необхідні NuGet-пакети
dotnet add package Amazon.Lambda.Core
dotnet add package Amazon.Lambda.Serialization.SystemTextJson
dotnet add package Amazon.Lambda.S3Events
dotnet add package AWSSDK.S3
dotnet add package SixLabors.ImageSharp

Крок 4: Деплой та конфігурація тригера

Тепер ми скомпілюємо код та налаштуємо автоматичний виклик Lambda при завантаженні файлу в S3.

  1. Деплой коду:
    • Виконайте dotnet lambda package локально, щоб отримати function.zip.
    • В консолі Lambda натисніть Create function.
    • Назва: ImageProcessor, Runtime: .NET 10.
    • Permissions: оберіть Use an existing role та вкажіть ImageProcessorRole.
    • Після створення у вкладці Code натисніть Upload from.zip file та оберіть ваш архів.
    • У Runtime settings змініть Handler на: ImageProcessor::ImageProcessor.Lambda.Function::FunctionHandler.
  2. Налаштування тригера:
    • На сторінці функції натисніть + Add trigger.
    • Оберіть S3. Bucket: kostyl-dev-source-images.
    • Event type: All object create events.
    • Підтвердіть згоду (Recursive invocation warning) та натисніть Add.

Крок 5: Тестування та моніторинг

Завантаження тестового файлу

# Завантажуємо будь-яке зображення у source bucket
aws s3 cp my-photo.jpg s3://kostyl-dev-source-images/photo-1.jpg

Перевірка результату

# Перевіряємо, чи з'явилася мініатюра у другому кошику
aws s3 ls s3://kostyl-dev-processed-images/thumbnails/

Моніторинг логів

# Дивимось логи виконання у реальному часі
aws logs tail /aws/lambda/ImageProcessor \
    --follow
У реальних проектах рекомендується використовувати AWS SAM або Terraform для опису цієї інфраструктури як коду (IaC), щоб уникнути ручного введення команд CLI.

Практичний воркшоп: Проектування Serverless URL Shortener (API + DynamoDB)

Цей розділ присвячений побудові високонавантаженої системи скорочення посилань. Проект демонструє синергію синхронного HTTP API та персистентного сховища NoSQL, реалізуючи патерн «Compute-as-a-Service» з мінімальною латентністю.

Архітектурна парадигма та потік даних

Система базується на трирівневій serverless-архітектурі:

  1. Інтерфейсний рівень (API Gateway): забезпечує HTTP-ендпоінти та автоматичну десеріалізацію запитів.
  2. Логічний рівень (Lambda .NET 10): виконує бізнес-логіку (генерація кодів, валідація URL) та взаємодію з базою даних.
  3. Рівень зберігання (DynamoDB): забезпечує атомарне збереження пар «Код — Оригінальний URL» з гарантованим часом доступу < 10ms.
Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

title "Архітектура Serverless URL Shortener"

actor "Клієнт" as CL
rectangle "API Gateway (HTTP API)" as AGW #dbeafe
rectangle "AWS Lambda (.NET 10)\nUrlShortenerFunction" as LAM #d1fae5
database "Amazon DynamoDB\nUrlMappingTable" as DDB #f3e8ff

CL -> AGW : 1. POST /shorten {url: "..."}
AGW -> LAM : 2. Invoke (Event JSON)
LAM -> DDB : 3. PutItem (ID, LongUrl)
LAM --> AGW : 4. Return {shortUrl: "..."}
AGW --> CL : 5. HTTP 201 Created

CL -> AGW : 6. GET /{shortCode}
AGW -> LAM : 7. Invoke (Path Parameter)
LAM -> DDB : 8. GetItem (ID)
LAM --> AGW : 9. HTTP 302 Redirect (Location)
AGW --> CL : 10. Redirect to Target
@enduml

Крок 1: Проектування схеми даних (DynamoDB)

Нам потрібна таблиця з високою пропускною здатністю для ключ-значення запитів.

  1. Перейдіть до DynamoDBTablesCreate table.
  2. Table name: UrlMappingTable.
  3. Partition key: ShortCode (String).
  4. Table settings: Default settings (On-Demand capacity).
  5. Натисніть Create table.

Крок 2: Дефініція політик доступу (IAM)

Lambda потребує гранулярних прав на виконання операцій GetItem та PutItem над створеною таблицею.

  1. Створіть роль UrlShortenerRole для сервісу Lambda.
  2. Додайте Inline Policy з наступним JSON:
    {
        "Version": "2012-10-17",
        "Statement": [
            {
                "Effect": "Allow",
                "Action": ["dynamodb:GetItem", "dynamodb:PutItem"],
                "Resource": "arn:aws:dynamodb:eu-central-1:*:table/UrlMappingTable"
            },
            {
                "Effect": "Allow",
                "Action": ["logs:*"],
                "Resource": "arn:aws:logs:*:*:*"
            }
        ]
    }
    

Крок 3: Реалізація бізнес-логіки (.NET 10)

Для чистоти коду ми використаємо Lambda Annotations, що дозволяє будувати API у декларативному стилі.

# Створюємо проект
dotnet new lambda.Annotations -n UrlShortener.Lambda
cd UrlShortener.Lambda

# Додаємо SDK для DynamoDB
dotnet add package AWSSDK.DynamoDBv2

Крок 4: Деплоймент та інтеграція з API Gateway

# 1. Деплой функції (Amazon.Lambda.Tools автоматично налаштує API Gateway на основі анотацій)
dotnet lambda deploy-serverless --stack-name url-shortener-stack \
    --s3-bucket my-deployment-bucket \
    --region eu-central-1

Перевірка редиректу

# Відкрийте у браузері або через curl -I
curl -I https://api-id.execute-api.eu-central-1.amazonaws.com/a1b2c3d4

::

Академічний висновок

Даний воркшоп демонструє реалізацію Stateless Backend з використанням зовнішнього State Store (DynamoDB). Завдяки використанню .NET 10 та Native AOT (опційно), система забезпечує передбачувану продуктивність при нульових витратах у стані простою, що є еталоном сучасної хмарної архітектури.

Крок 6: Очистка ресурсів

Не забудьте видалити всі створені ресурси після завершення роботи з воркшопом, щоб уникнути небажаних витрат на AWS-рахунку.
# 1. Видаляємо CloudFormation стек (видаляє Lambda + API Gateway + IAM ролі автоматично)
aws cloudformation delete-stack \
    --stack-name url-shortener-stack \
    --region eu-central-1

# 2. Видаляємо DynamoDB таблицю
aws dynamodb delete-table \
    --table-name UrlMappingTable \
    --region eu-central-1

# 3. Перевіряємо, що стек видалено
aws cloudformation describe-stacks \
    --stack-name url-shortener-stack \
    --region eu-central-1
# Очікуємо: "An error occurred (ValidationError): Stack with id url-shortener-stack does not exist"

Фінальний висновок

AWS Lambda — це не просто «функція в хмарі». Це архітектурний підхід, що кардинально змінює спосіб мислення про серверну інфраструктуру. Від простого Hello World до розподілених систем обробки зображень та URL-шортенерів — ми пройшли повний шлях.

Ключові принципи, які варто запам'ятати

Stateless за замовчуванням

Lambda функції не мають стану між викликами. Execution Environment може бути перероблено у будь-який момент. Весь стан зберігайте зовні: DynamoDB, S3, ElastiCache.

Cold Start — це компроміс

Cold Start є невід'ємною частиною serverless-моделі. Для більшості застосунків він не є проблемою. Для SLA-критичних систем використовуйте Provisioned Concurrency або Function URL + SnapStart.

IAM — найменші привілеї

Кожна Lambda функція повинна мати лише ті дозволи, які їй необхідні. Це принцип Least Privilege — основа безпеки хмарних систем.

Observability з CloudWatch

Без метрик і логів Lambda — це «чорна скринька». Завжди налаштовуйте CloudWatch Alarms для Errors, Throttles та Duration. Використовуйте Structured Logging для зручного пошуку.

Вартість = час × пам'ять

Оптимізація Lambda — це завжди баланс між часом виконання та обсягом пам'яті. Профілюйте реальне навантаження за допомогою AWS Lambda Power Tuning перед вибором параметрів у Production.

IaC для всього

Ніколи не створюйте Lambda-ресурси вручну в Production. AWS SAM, Terraform або CDK дозволяють версіонувати інфраструктуру, відтворювати середовища та уникати «snowflake servers».

Що далі?

Після освоєння основ Lambda, наступні кроки у вивченні serverless на AWS:

ТехнологіяЩо вивчити
AWS SAMInfrastructure as Code для serverless: шаблони, локальне тестування
AWS Step FunctionsОркестрація складних multi-step бізнес-процесів
Amazon EventBridgeEvent-driven архітектура між сервісами та SaaS-інтеграції
AWS App RunnerServerless контейнери для довготривалих HTTP-сервісів
Amazon Bedrock + LambdaІнтеграція AI/ML моделей у serverless пайплайни
Найкращий спосіб закріпити знання — побудувати власний реальний проект. Оберіть будь-яку задачу автоматизації (обробка файлів, webhook-handler, scheduled job) та реалізуйте її на AWS Lambda з .NET. Практика швидко розставить усе на свої місця.
Copyright © 2026