Spis treści 12 sekcji
- Architektura naszego API
- Krok 1: Tworzymy tabelę DynamoDB
- Krok 2: Tworzymy rolę IAM dla Lambdy
- Krok 3: Tworzymy funkcję Lambda
- Krok 4: Tworzymy API Gateway
- Krok 5: Testujemy API
- Cold starty i wydajność
- Ile to kosztuje?
- Monitoring i debugowanie
- Zabezpieczanie API
- Najczęściej zadawane pytania (FAQ)
- Następny krok
- Zbudujesz działające REST API (CRUD) z AWS Lambda, API Gateway i DynamoDB w ~30 minut
- Lambda handler w Pythonie - pełny kod gotowy do wklejenia
- Free Tier: 1 mln requestów Lambda + 1 mln wywołań API Gateway miesięcznie za darmo
- Każdy krok pokazany dwoma metodami: Panel AWS i AWS CLI
- Na końcu testujesz API za pomocą curl
Kiedy pierwszy raz usłyszałem o serverless, pomyślałem: "kolejny buzzword". Potem postawiłem swoje pierwsze API na Lambda i API Gateway - dosłownie w pół godziny miałem działający endpoint, zero serwerów do zarządzania i rachunek wynoszący 0 zł. Od tamtej pory serverless to moje domyślne podejście do małych i średnich API.
W tym artykule zbudujemy razem kompletne REST API do zarządzania notatkami. Będziemy mieli endpointy do tworzenia, odczytywania, aktualizowania i usuwania notatek (CRUD). Jako bazę danych wykorzystamy DynamoDB, a całość postawimy w ramach AWS Free Tier.
aws cli poleceniem aws configure.Architektura naszego API
Zanim zaczniemy klikać, zobaczmy co dokładnie budujemy:
- API Gateway - przyjmuje requesty HTTP (GET, POST, PUT, DELETE) i przekazuje je do Lambdy
- Lambda - funkcja w Pythonie, która przetwarza request i komunikuje się z bazą danych
- DynamoDB - baza NoSQL przechowująca nasze notatki
- IAM Role - uprawnienia pozwalające Lambdzie czytać i zapisywać dane w DynamoDB
Klient wysyła request HTTP do API Gateway, API Gateway wywołuje Lambdę, Lambda czyta/zapisuje dane w DynamoDB i zwraca odpowiedź. Proste i skalowalne - od 1 do 10 000 requestów na sekundę bez żadnej konfiguracji.
Krok 1: Tworzymy tabelę DynamoDB
Zaczynamy od bazy danych, bo Lambda będzie jej potrzebować od pierwszego uruchomienia.
- Otwórz konsolę AWS i przejdź do usługi DynamoDB
- Kliknij Create table
- Table name:
ServerlessNotes - Partition key:
noteId(typ: String) - Zostaw resztę ustawień domyślnie (On-demand capacity) i kliknij Create table
aws dynamodb create-table \
--table-name ServerlessNotes \
--attribute-definitions AttributeName=noteId,AttributeType=S \
--key-schema AttributeName=noteId,KeyType=HASH \
--billing-mode PAY_PER_REQUEST \
--region eu-central-1
Krok 2: Tworzymy rolę IAM dla Lambdy
Lambda potrzebuje uprawnień do zapisywania logów w CloudWatch oraz do operacji na tabeli DynamoDB. Zgodnie z zasadą least privilege, dajemy tylko to co niezbędne. Więcej o bezpieczeństwie IAM znajdziesz w poradniku IAM.
- Przejdź do IAM → Roles → Create role
- Trusted entity: AWS service, Use case: Lambda
- Dodaj politykę AWSLambdaBasicExecutionRole (logi CloudWatch)
- Nazwij rolę:
NotesApiLambdaRolei utwórz ją - Otwórz utworzoną rolę, kliknij Add permissions → Create inline policy
- Wybierz JSON i wklej politykę dostępu do DynamoDB (poniżej)
# Utwórz trust policy
cat > trust-policy.json << 'EOF'
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {"Service": "lambda.amazonaws.com"},
"Action": "sts:AssumeRole"
}]
}
EOF
# Utwórz rolę
aws iam create-role \
--role-name NotesApiLambdaRole \
--assume-role-policy-document file://trust-policy.json
# Dodaj politykę logowania
aws iam attach-role-policy \
--role-name NotesApiLambdaRole \
--policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
# Dodaj inline policy dla DynamoDB
cat > dynamodb-policy.json << 'EOF'
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": [
"dynamodb:PutItem",
"dynamodb:GetItem",
"dynamodb:UpdateItem",
"dynamodb:DeleteItem",
"dynamodb:Scan"
],
"Resource": "arn:aws:dynamodb:eu-central-1:TWOJE_ACCOUNT_ID:table/ServerlessNotes"
}]
}
EOF
aws iam put-role-policy \
--role-name NotesApiLambdaRole \
--policy-name DynamoDBNotesAccess \
--policy-document file://dynamodb-policy.json
TWOJE_ACCOUNT_ID na rzeczywiste ID Twojego konta AWS. Znajdziesz je w prawym górnym rogu konsoli AWS. Nigdy nie używaj "Resource": "*" w produkcji.Krok 3: Tworzymy funkcję Lambda
Czas na serce naszego API. Napiszemy jedną funkcję Lambda, która obsłuży wszystkie operacje CRUD. To podejście nazywa się "monolith Lambda" i sprawdza się świetnie przy małych API.
Kod Lambda handlera (Python 3.12)
import json
import boto3
import uuid
from datetime import datetime, timezone
dynamodb = boto3.resource("dynamodb")
table = dynamodb.Table("ServerlessNotes")
def lambda_handler(event, context):
http_method = event["httpMethod"]
path = event.get("pathParameters")
try:
if http_method == "GET" and path and path.get("noteId"):
return get_note(path["noteId"])
elif http_method == "GET":
return list_notes()
elif http_method == "POST":
return create_note(json.loads(event["body"]))
elif http_method == "PUT" and path:
return update_note(path["noteId"], json.loads(event["body"]))
elif http_method == "DELETE" and path:
return delete_note(path["noteId"])
else:
return response(400, {"error": "Nieobslugiwana operacja"})
except Exception as e:
print(f"Blad: {str(e)}")
return response(500, {"error": "Wewnetrzny blad serwera"})
def create_note(data):
note = {
"noteId": str(uuid.uuid4()),
"title": data["title"],
"content": data.get("content", ""),
"createdAt": datetime.now(timezone.utc).isoformat(),
"updatedAt": datetime.now(timezone.utc).isoformat()
}
table.put_item(Item=note)
return response(201, note)
def get_note(note_id):
result = table.get_item(Key={"noteId": note_id})
if "Item" not in result:
return response(404, {"error": "Notatka nie znaleziona"})
return response(200, result["Item"])
def list_notes():
result = table.scan()
return response(200, result.get("Items", []))
def update_note(note_id, data):
result = table.update_item(
Key={"noteId": note_id},
UpdateExpression="SET title = :t, content = :c, updatedAt = :u",
ExpressionAttributeValues={
":t": data["title"],
":c": data.get("content", ""),
":u": datetime.now(timezone.utc).isoformat()
},
ReturnValues="ALL_NEW"
)
return response(200, result["Attributes"])
def delete_note(note_id):
table.delete_item(Key={"noteId": note_id})
return response(200, {"message": "Notatka usunieta"})
def response(status_code, body):
return {
"statusCode": status_code,
"headers": {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*"
},
"body": json.dumps(body, default=str)
}
Access-Control-Allow-Origin: * w odpowiedziach. Bez niego frontend na innej domenie nie będzie mógł korzystać z API (blokada CORS). Sam nagłówek nie wystarczy: przy PUT/DELETE i JSON przeglądarka wysyła najpierw request preflight (OPTIONS), więc w API Gateway użyj opcji Enable CORS na zasobach - doda metodę OPTIONS automatycznie. W produkcji zamień * na konkretną domenę.- Przejdź do Lambda → Create function
- Wybierz Author from scratch
- Function name:
NotesApiHandler - Runtime: Python 3.12
- Execution role: Use an existing role →
NotesApiLambdaRole - Kliknij Create function
- W edytorze kodu wklej cały handler powyżej i kliknij Deploy
- W zakładce Configuration → General configuration ustaw Timeout na 10 sekund
# Zapisz kod jako lambda_function.py, potem spakuj
zip function.zip lambda_function.py
# Utwórz funkcję (zamień ACCOUNT_ID)
aws lambda create-function \
--function-name NotesApiHandler \
--runtime python3.12 \
--handler lambda_function.lambda_handler \
--role arn:aws:iam::ACCOUNT_ID:role/NotesApiLambdaRole \
--zip-file fileb://function.zip \
--timeout 10 \
--region eu-central-1
Krok 4: Tworzymy API Gateway
Teraz wystawimy naszą Lambdę na świat przez API Gateway. Stworzymy REST API z odpowiednimi ścieżkami.
- Przejdź do API Gateway → Create API
- Wybierz REST API (nie REST API Private) → Build
- API name:
NotesApi, Endpoint Type: Regional - Kliknij Create API
- Utwórz resource
/notes: kliknij Create resource, Resource name:notes - Na
/notesdodaj metody GET i POST: kliknij Create method - Dla każdej metody: Integration type: Lambda Function, Lambda Function:
NotesApiHandler, zaznacz Use Lambda Proxy integration - Utwórz child resource
{noteId}pod/notes: Resource Path:{noteId} - Na
/notes/{noteId}dodaj metody GET, PUT, DELETE - tak samo z Lambda Proxy - Kliknij Deploy API, Stage: New stage, Stage name:
prod
# Utwórz REST API
API_ID=$(aws apigateway create-rest-api \
--name NotesApi \
--endpoint-configuration types=REGIONAL \
--query "id" --output text)
# Pobierz root resource ID
ROOT_ID=$(aws apigateway get-resources \
--rest-api-id $API_ID \
--query "items[?path=='/'].id" --output text)
# Utwórz /notes
NOTES_ID=$(aws apigateway create-resource \
--rest-api-id $API_ID \
--parent-id $ROOT_ID \
--path-part notes \
--query "id" --output text)
# Utwórz /notes/{noteId}
NOTE_ID=$(aws apigateway create-resource \
--rest-api-id $API_ID \
--parent-id $NOTES_ID \
--path-part "{noteId}" \
--query "id" --output text)
# Dla kazdej metody (przyklad GET /notes):
aws apigateway put-method \
--rest-api-id $API_ID \
--resource-id $NOTES_ID \
--http-method GET \
--authorization-type NONE
aws apigateway put-integration \
--rest-api-id $API_ID \
--resource-id $NOTES_ID \
--http-method GET \
--type AWS_PROXY \
--integration-http-method POST \
--uri "arn:aws:apigateway:eu-central-1:lambda:path/2015-03-31/functions/arn:aws:lambda:eu-central-1:ACCOUNT_ID:function:NotesApiHandler/invocations"
# Powtórz put-method i put-integration dla:
# POST /notes, GET /notes/{noteId},
# PUT /notes/{noteId}, DELETE /notes/{noteId}
# Dodaj permission dla API Gateway
aws lambda add-permission \
--function-name NotesApiHandler \
--statement-id apigateway-access \
--action lambda:InvokeFunction \
--principal apigateway.amazonaws.com \
--source-arn "arn:aws:execute-api:eu-central-1:ACCOUNT_ID:$API_ID/*"
# Deploy
aws apigateway create-deployment \
--rest-api-id $API_ID \
--stage-name prod
echo "API URL: https://$API_ID.execute-api.eu-central-1.amazonaws.com/prod"
add-permission powyżej). W konsoli AWS robi się to automatycznie.Krok 5: Testujemy API
Twoje API powinno być już dostępne pod adresem wyświetlonym po deployu. Skopiuj URL i przetestuj wszystkie operacje:
# Zamień YOUR_API_URL na rzeczywisty URL z API Gateway
API="https://xxxxxxxxxx.execute-api.eu-central-1.amazonaws.com/prod"
# Utwórz notatkę
curl -X POST "$API/notes" \
-H "Content-Type: application/json" \
-d '{"title": "Moja pierwsza notatka", "content": "Serverless dziala!"}'
# Lista wszystkich notatek
curl "$API/notes"
# Pobierz konkretną notatkę (zamień NOTE_ID)
curl "$API/notes/NOTE_ID"
# Zaktualizuj notatkę
curl -X PUT "$API/notes/NOTE_ID" \
-H "Content-Type: application/json" \
-d '{"title": "Zaktualizowana notatka", "content": "Nowa tresc"}'
# Usun notatkę
curl -X DELETE "$API/notes/NOTE_ID"
Cold starty i wydajność
Jedyne "ale" serverless to cold starty. Kiedy Lambda nie była wywoływana przez kilka minut, AWS musi uruchomić nowy kontener - to dodaje 200-500ms do pierwszego requesta. Z mojego doświadczenia przy codziennej pracy z Lambdą na projektach klientów ClearScale, cold starty w Pythonie są jednymi z najkrótszych.
Jak sobie z nimi radzić:
- Provisioned Concurrency - utrzymuje "ciepłe" instancje (koszt ~$15/miesiąc za jedną), stosuj tylko jeśli potrzebujesz gwarantowanego czasu odpowiedzi poniżej 100ms
- Małe paczki - im mniej zależności, tym szybszy start. Nasz handler importuje tylko boto3 (jest preinstalowany), więc start jest błyskawiczny
- Trzymaj połączenie do DynamoDB poza handlerem - właśnie to zrobiliśmy deklarując
dynamodbitablena górze pliku
Więcej o różnicach między serverless a tradycyjnym podejściem znajdziesz w artykule EC2 vs Lambda.
Ile to kosztuje?
To jest najlepsza część. Policzmy koszty dla typowego side-projectu lub małego MVP:
| Usługa | Free Tier (miesięcznie) | Koszt po przekroczeniu |
|---|---|---|
| Lambda | 1 mln requestów + 400 000 GB-s | $0.20 / 1 mln requestów |
| API Gateway | 1 mln wywołań (przez 12 miesięcy) | $3.50 / 1 mln wywołań |
| DynamoDB (On-demand) | 25 GB storage, 25 WCU, 25 RCU | $1.25 / 1 mln zapisów |
Realistyczny scenariusz: Twoje API obsługuje 10 000 requestów dziennie (300 000/miesiąc). Koszt? Dokładnie $0 - mieścisz się w Free Tier. Nawet przy 100 000 requestów dziennie (3 mln/miesiąc) zapłacisz około $4-7 miesięcznie. Spróbuj osiągnąć coś podobnego na EC2 - sam koszt instancji t3.micro to już ~$8/miesiąc, niezależnie od ruchu.
Monitoring i debugowanie
Po wdrożeniu API warto skonfigurować podstawowy monitoring w CloudWatch:
- Lambda Metrics - Invocations, Duration, Errors, Throttles - wszystko dostępne automatycznie
- API Gateway Metrics - 4XX Errors, 5XX Errors, Latency, Count
- Alarm - ustaw alarm na 5XX Errors > 0 w ciągu 5 minut, żeby dostać powiadomienie gdy coś się zepsuje
W Lambda console, zakładka Monitor pokazuje wykresy w czasie rzeczywistym. Kliknij "View CloudWatch logs" żeby zobaczyć logi z print() w Twoim handlerze. To bezcenne przy debugowaniu.
Zabezpieczanie API
Nasze API jest teraz publiczne - każdy znający URL może z niego korzystać. W produkcji warto dodać uwierzytelnianie:
- API Key - najprostsze rozwiązanie, wystarczy dla wewnętrznych API i prototypów
- Lambda Authorizer - własna logika autoryzacji (np. weryfikacja JWT tokena)
- Cognito User Pools - pełne zarządzanie użytkownikami z rejestracją, logowaniem, MFA
- IAM Authorization - dla komunikacji między serwisami AWS
Dodatkowo warto włączyć throttling w API Gateway (domyślnie 10 000 requestów/sekundę na konto). Dla naszego API ustaw rozsądny limit, np. 100 requestów/sekundę, żeby uchronić się przed przypadkowym lub celowym przeciążeniem.
Najczęściej zadawane pytania (FAQ)
Czy mogę użyć Node.js zamiast Pythona w Lambdzie?
Tak. Lambda obsługuje Node.js, Python, Java, Go, .NET i Ruby. Python i Node.js mają najkrótsze cold starty. Logika CRUD będzie wyglądać bardzo podobnie - zmieni się tylko składnia i SDK (AWS SDK for JavaScript v3, np. @aws-sdk/client-dynamodb i @aws-sdk/lib-dynamodb, zamiast boto3).
Jak wygląda skalowanie? Czy muszę coś konfigurować?
Nie musisz nic konfigurować. Lambda automatycznie skaluje się od 0 do tysięcy równoległych instancji. Domyślny limit to 1000 concurrent executions na region - możesz go zwiększyć przez AWS Support. API Gateway domyślnie obsługuje 10 000 requestów/sekundę.
Co jeśli moja Lambda przekroczy limit czasu (timeout)?
API Gateway zwróci błąd 504 Gateway Timeout. Domyślny timeout Lambdy to 3 sekundy, my ustawiliśmy 10. Maksymalny limit to 15 minut, ale API Gateway ma limit integracji - domyślnie 29 sekund (dla regionalnych REST API można go zwiększyć na wniosek, kosztem niższego limitu throttlingu). Jeśli operacja trwa dłużej, rozważ wzorzec asynchroniczny z SQS.
Czy mogę podłączyć własną domenę do API Gateway?
Tak. W API Gateway przejdź do Custom domain names, dodaj swoją domenę (np. api.twojadomena.pl), wgraj certyfikat SSL z ACM i skonfiguruj rekord CNAME w DNS. Cały proces zajmuje około 15 minut.
Serverless vs EC2 - kiedy wybrać które?
Serverless (Lambda) sprawdza się przy nieregularnym ruchu, MVP, mikroserwisach i prostych API. EC2 lepiej sprawdza się przy stałym, wysokim ruchu (powyżej milionów requestów dziennie), długo trwających procesach i aplikacjach wymagających pełnej kontroli nad systemem. Szczegółowe porównanie znajdziesz w artykule EC2 vs Lambda.
Następny krok
Masz działające REST API, ale to dopiero początek. Oto co warto zrobić dalej:
- Dodaj uwierzytelnianie - API Key lub Cognito, w zależności od potrzeb
- Wdróż infrastrukturę jako kod - przepisz wszystko do AWS SAM lub CloudFormation, żeby móc odtworzyć środowisko jedną komendą
- Dodaj walidację - sprawdzaj przychodzące dane w Lambdzie (np. czy title nie jest pusty)
- Skonfiguruj CI/CD - automatyczny deploy po pushu do repozytorium
- Rozszerz monitoring - dodaj X-Ray tracing żeby śledzić czas każdej operacji DynamoDB
Jeśli dopiero zaczynasz przygodę z AWS, sprawdź kompletny przewodnik dla początkujących. A jeśli zastanawiasz się nad bezpieczeństwem swojego konta, koniecznie przeczytaj poradnik IAM.
Jeśli chcesz rozbudować swoje serverless API o powiadomienia i kolejki, sprawdź System powiadomień z SNS + SQS + Lambda.
Masz pytania? Napisz do mnie - chętnie pomogę z Twoim pierwszym serverless projektem.
