Serverless

Serverless REST API a nulláról: API Gateway + Lambda + DynamoDB, végig Terraformmal

Egy teljes, production-kész serverless API felépítése lépésről lépésre: HTTP API, Lambda-integráció, DynamoDB single-table modell, IAM least-privilege és megfigyelhetőség — minden sorban Terraformmal.

Röviden: Egy teljes, production-kész serverless API felépítése lépésről lépésre: HTTP API, Lambda-integráció, DynamoDB single-table modell, IAM least-privilege és megfigyelhetőség — minden sorban Terraformmal.

Egy serverless REST API-t sokan „pár kattintásból” állítanak össze a konzolon, aztán fél év múlva senki nem tudja, mi hogyan lett bekötve. Ebben a cikkben végigépítünk egy teljes, reprodukálható API-t Terraformmal: HTTP API a belépési pont, Lambda a logika, DynamoDB az adat, IAM a szigorú jogosultság, és CloudWatch a láthatóság. A cél nem a leggyorsabb demó, hanem egy olyan alap, amit másfél év múlva is bátran módosítasz.

A cél-architektúra

A kliens egy HTTP API-hoz beszél, ami minden útvonalat egyetlen Lambda-függvényhez proxyzik (Lambda proxy integráció). A függvény egy DynamoDB táblát olvas és ír single-table modellben. A jogosultságokat egy szűkre szabott IAM-role adja, a naplók és metrikák a CloudWatchba folynak.

Miért HTTP API és nem REST API?

Az API Gateway kétféle terméket kínál: a régebbi REST API-t és az újabb HTTP API-t. A legtöbb új projektnek a HTTP API a jó választás: olcsóbb, alacsonyabb a latenciája, és natívan támogatja a JWT-alapú authorizert. A REST API akkor indokolt, ha API-kulcsokra, use-plan alapú kvótára, request/response transzformációra vagy WAF-integrációra van szükséged közvetlenül a gatewayen.

SzempontHTTP APIREST API
Ár (millió kérésenként)alacsonyabbmagasabb
Latenciakisebb overheadnagyobb
JWT authorizernatívLambda authorizer kell
Request/response mappingkorlátozottteljes
API-kulcs + usage plannincsvan
Mikor válaszd?új, egyszerű JSON APIlegacy, kvóta, transzformáció

A DynamoDB adatmodell

A single-table design lényege, hogy egyetlen táblában, generikus PK/SK kulcsokkal tárolunk több entitástípust. Egy egyszerű „feladatok egy felhasználóhoz” modellnél a partíciós kulcs a felhasználó, a rendezési kulcs pedig a feladat azonosítója.

EntitásPKSKtovábbi attribútumok
FeladatUSER#42TASK#01H...title, done, createdAt
FelhasználóUSER#42PROFILEemail, name

Így egyetlen Query-vel lekérhető egy felhasználó összes feladata (PK = USER#42 AND begins_with(SK, 'TASK#')), és pont-lekéréssel egy konkrét feladat is.

A Terraform alap: provider és tábla

Kezdjük a provider rögzítésével és a DynamoDB táblával. On-demand (PAY_PER_REQUEST) kapacitást használunk, hogy ne kelljen kapacitást tervezni — kiszámíthatatlan terhelésnél ez a biztonságos alapértelmezés.

hcl
terraform {
  required_version = ">= 1.9"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.60"
    }
  }
}

provider "aws" {
  region = var.region
}

resource "aws_dynamodb_table" "tasks" {
  name         = "${var.project}-tasks"
  billing_mode = "PAY_PER_REQUEST"
  hash_key     = "PK"
  range_key    = "SK"

  attribute {
    name = "PK"
    type = "S"
  }
  attribute {
    name = "SK"
    type = "S"
  }

  point_in_time_recovery {
    enabled = true
  }

  tags = local.tags
}

A Lambda és a szigorú IAM-role

A függvény kap egy role-t, ami CSAK az adott tábla olvasására/írására jogosít — nem dynamodb:* az egész fiókon. Ez a least-privilege elv, és Terraformban pont ilyen jól látszik, mit engedünk meg.

hcl
resource "aws_iam_role" "lambda" {
  name = "${var.project}-lambda"
  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Action    = "sts:AssumeRole"
      Effect    = "Allow"
      Principal = { Service = "lambda.amazonaws.com" }
    }]
  })
}

data "aws_iam_policy_document" "table_access" {
  statement {
    actions = [
      "dynamodb:GetItem",
      "dynamodb:PutItem",
      "dynamodb:Query",
      "dynamodb:UpdateItem",
      "dynamodb:DeleteItem",
    ]
    resources = [aws_dynamodb_table.tasks.arn]
  }
}

resource "aws_iam_role_policy" "table_access" {
  name   = "table-access"
  role   = aws_iam_role.lambda.id
  policy = data.aws_iam_policy_document.table_access.json
}

resource "aws_iam_role_policy_attachment" "logs" {
  role       = aws_iam_role.lambda.name
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
}

resource "aws_lambda_function" "api" {
  function_name = "${var.project}-api"
  role          = aws_iam_role.lambda.arn
  runtime       = "nodejs20.x"
  handler       = "index.handler"
  filename      = data.archive_file.lambda.output_path
  source_code_hash = data.archive_file.lambda.output_base64sha256
  timeout       = 10
  memory_size   = 256

  environment {
    variables = {
      TABLE_NAME = aws_dynamodb_table.tasks.name
    }
  }

  tags = local.tags
}

A HTTP API bekötése

A HTTP API-t egy aws_apigatewayv2_api írja le, a Lambda-integrációt pedig egy proxy-route. A $default route mindent a függvényhez irányít — a routingot magában a kódban intézzük, ami egyszerű API-nál teljesen elfogadható.

hcl
resource "aws_apigatewayv2_api" "http" {
  name          = "${var.project}-http"
  protocol_type = "HTTP"
}

resource "aws_apigatewayv2_integration" "lambda" {
  api_id                 = aws_apigatewayv2_api.http.id
  integration_type       = "AWS_PROXY"
  integration_uri        = aws_lambda_function.api.invoke_arn
  payload_format_version = "2.0"
}

resource "aws_apigatewayv2_route" "default" {
  api_id    = aws_apigatewayv2_api.http.id
  route_key = "$default"
  target    = "integrations/${aws_apigatewayv2_integration.lambda.id}"
}

resource "aws_apigatewayv2_stage" "default" {
  api_id      = aws_apigatewayv2_api.http.id
  name        = "$default"
  auto_deploy = true
}

resource "aws_lambda_permission" "apigw" {
  statement_id  = "AllowAPIGatewayInvoke"
  action        = "lambda:InvokeFunction"
  function_name = aws_lambda_function.api.function_name
  principal     = "apigateway.amazonaws.com"
  source_arn    = "${aws_apigatewayv2_api.http.execution_arn}/*/*"
}

A aws_lambda_permission gyakran elfelejtett, de kötelező lépés: e nélkül a gateway nem hívhatja a függvényt, és 500-as hibát kapsz látszólag ok nélkül.

A kérés útja végponttól végpontig

Érdemes fejben tartani, mi történik egyetlen kéréssel — ez segít a hibakeresésben, amikor valami nem úgy megy, ahogy várod.

Költség: mit fizetsz valójában?

Serverlessnél a vonzó rész, hogy tétlenül szinte semmibe nem kerül. Egy közepes forgalmú API havi költsége jellemzően a kérésszám és a Lambda futásidő függvénye — nem előre lefoglalt kapacitásé.

KomponensElszámolásMegjegyzés
HTTP APIkérésenkéntnincs alapdíj
LambdaGB-másodperc + kéréstétlenül 0
DynamoDB (on-demand)olvasott/írt egységenkéntnincs kapacitástervezés
CloudWatchnapló-GB + metrikaa log-retention állítsd be!

Mit jelent ez neked?

Ez az öt erőforrás — tábla, role, policy, függvény, API — egy teljes, reprodukálható serverless backend. A lényeg nem a méret, hanem hogy minden döntés látszik a kódban: mit engedsz meg (IAM), hogyan skálázódik (on-demand), és hogyan figyeled (CloudWatch). Ha innen indulsz, a következő lépés a JWT-authorizer, a több route és a CI/CD — de az alap már production-kész, verziózható és auditálható. A terraform apply mindig ugyanazt adja, és ez a nyugalom a serverless igazi haszna.