Projet 4 : Architecture Serverless
Contexte
Vous développez une API pour une application mobile de gestion de tâches. L'architecture doit être :
- Sans serveur à gérer
- Scalable automatiquement
- Pay-per-use
Exigences
- API REST avec CRUD complet
- Authentification via Cognito
- Base de données DynamoDB
- Cache API Gateway
- Monitoring avec X-Ray
Architecture cible
Structure du projet
projet-serverless/
├── main.tf
├── variables.tf
├── outputs.tf
├── versions.tf
├── api-gateway.tf
├── lambda.tf
├── dynamodb.tf
├── cognito.tf
├── iam.tf
├── lambda-code/
│ ├── create_task/
│ │ └── index.py
│ ├── get_tasks/
│ │ └── index.py
│ ├── update_task/
│ │ └── index.py
│ └── delete_task/
│ └── index.py
└── terraform.tfvars
Implémentation
variables.tf
variable "region" {
description = "AWS Region"
type = string
default = "eu-west-1"
}
variable "project_name" {
description = "Project name"
type = string
}
variable "environment" {
description = "Environment"
type = string
}
variable "lambda_runtime" {
description = "Lambda runtime"
type = string
default = "python3.11"
}
variable "lambda_memory" {
description = "Lambda memory in MB"
type = number
default = 256
}
variable "lambda_timeout" {
description = "Lambda timeout in seconds"
type = number
default = 30
}
variable "enable_xray" {
description = "Enable X-Ray tracing"
type = bool
default = true
}
variable "enable_api_cache" {
description = "Enable API Gateway cache"
type = bool
default = true
}
variable "api_cache_size" {
description = "API Gateway cache size in GB"
type = string
default = "0.5"
}
dynamodb.tf
resource "aws_dynamodb_table" "tasks" {
name = "${local.name_prefix}-tasks"
billing_mode = "PAY_PER_REQUEST"
hash_key = "userId"
range_key = "taskId"
attribute {
name = "userId"
type = "S"
}
attribute {
name = "taskId"
type = "S"
}
attribute {
name = "status"
type = "S"
}
attribute {
name = "dueDate"
type = "S"
}
# GSI pour requêtes par status
global_secondary_index {
name = "StatusIndex"
hash_key = "userId"
range_key = "status"
projection_type = "ALL"
}
# GSI pour requêtes par date d'échéance
global_secondary_index {
name = "DueDateIndex"
hash_key = "userId"
range_key = "dueDate"
projection_type = "ALL"
}
point_in_time_recovery {
enabled = var.environment == "production"
}
server_side_encryption {
enabled = true
}
ttl {
attribute_name = "expiresAt"
enabled = true
}
tags = {
Name = "${local.name_prefix}-tasks"
}
}
cognito.tf
resource "aws_cognito_user_pool" "main" {
name = "${local.name_prefix}-users"
username_attributes = ["email"]
auto_verified_attributes = ["email"]
password_policy {
minimum_length = 8
require_lowercase = true
require_numbers = true
require_symbols = true
require_uppercase = true
}
mfa_configuration = "OPTIONAL"
software_token_mfa_configuration {
enabled = true
}
account_recovery_setting {
recovery_mechanism {
name = "verified_email"
priority = 1
}
}
schema {
name = "email"
attribute_data_type = "String"
developer_only_attribute = false
mutable = true
required = true
string_attribute_constraints {
max_length = "256"
min_length = "1"
}
}
schema {
name = "name"
attribute_data_type = "String"
developer_only_attribute = false
mutable = true
required = false
string_attribute_constraints {
max_length = "256"
min_length = "1"
}
}
tags = {
Name = "${local.name_prefix}-users"
}
}
resource "aws_cognito_user_pool_client" "web" {
name = "${local.name_prefix}-web-client"
user_pool_id = aws_cognito_user_pool.main.id
generate_secret = false
explicit_auth_flows = [
"ALLOW_USER_PASSWORD_AUTH",
"ALLOW_USER_SRP_AUTH",
"ALLOW_REFRESH_TOKEN_AUTH"
]
supported_identity_providers = ["COGNITO"]
access_token_validity = 1
id_token_validity = 1
refresh_token_validity = 30
token_validity_units {
access_token = "hours"
id_token = "hours"
refresh_token = "days"
}
}
resource "aws_cognito_user_pool_client" "mobile" {
name = "${local.name_prefix}-mobile-client"
user_pool_id = aws_cognito_user_pool.main.id
generate_secret = false
explicit_auth_flows = [
"ALLOW_USER_SRP_AUTH",
"ALLOW_REFRESH_TOKEN_AUTH"
]
supported_identity_providers = ["COGNITO"]
access_token_validity = 24
id_token_validity = 24
refresh_token_validity = 90
token_validity_units {
access_token = "hours"
id_token = "hours"
refresh_token = "days"
}
}
iam.tf
# Lambda Execution Role
resource "aws_iam_role" "lambda" {
name = "${local.name_prefix}-lambda-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
Service = "lambda.amazonaws.com"
}
}
]
})
}
# CloudWatch Logs Policy
resource "aws_iam_role_policy" "lambda_logs" {
name = "${local.name_prefix}-lambda-logs"
role = aws_iam_role.lambda.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
]
Resource = "arn:aws:logs:*:*:*"
}
]
})
}
# DynamoDB Policy
resource "aws_iam_role_policy" "lambda_dynamodb" {
name = "${local.name_prefix}-lambda-dynamodb"
role = aws_iam_role.lambda.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"dynamodb:GetItem",
"dynamodb:PutItem",
"dynamodb:UpdateItem",
"dynamodb:DeleteItem",
"dynamodb:Query",
"dynamodb:Scan"
]
Resource = [
aws_dynamodb_table.tasks.arn,
"${aws_dynamodb_table.tasks.arn}/index/*"
]
}
]
})
}
# X-Ray Policy
resource "aws_iam_role_policy" "lambda_xray" {
count = var.enable_xray ? 1 : 0
name = "${local.name_prefix}-lambda-xray"
role = aws_iam_role.lambda.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"xray:PutTraceSegments",
"xray:PutTelemetryRecords"
]
Resource = "*"
}
]
})
}
lambda.tf
# Archiver le code Lambda
data "archive_file" "create_task" {
type = "zip"
source_dir = "${path.module}/lambda-code/create_task"
output_path = "${path.module}/.builds/create_task.zip"
}
data "archive_file" "get_tasks" {
type = "zip"
source_dir = "${path.module}/lambda-code/get_tasks"
output_path = "${path.module}/.builds/get_tasks.zip"
}
data "archive_file" "update_task" {
type = "zip"
source_dir = "${path.module}/lambda-code/update_task"
output_path = "${path.module}/.builds/update_task.zip"
}
data "archive_file" "delete_task" {
type = "zip"
source_dir = "${path.module}/lambda-code/delete_task"
output_path = "${path.module}/.builds/delete_task.zip"
}
# Lambda Functions
resource "aws_lambda_function" "create_task" {
filename = data.archive_file.create_task.output_path
function_name = "${local.name_prefix}-create-task"
role = aws_iam_role.lambda.arn
handler = "index.handler"
source_code_hash = data.archive_file.create_task.output_base64sha256
runtime = var.lambda_runtime
memory_size = var.lambda_memory
timeout = var.lambda_timeout
environment {
variables = {
TABLE_NAME = aws_dynamodb_table.tasks.name
ENVIRONMENT = var.environment
}
}
tracing_config {
mode = var.enable_xray ? "Active" : "PassThrough"
}
tags = {
Name = "${local.name_prefix}-create-task"
}
}
resource "aws_lambda_function" "get_tasks" {
filename = data.archive_file.get_tasks.output_path
function_name = "${local.name_prefix}-get-tasks"
role = aws_iam_role.lambda.arn
handler = "index.handler"
source_code_hash = data.archive_file.get_tasks.output_base64sha256
runtime = var.lambda_runtime
memory_size = var.lambda_memory
timeout = var.lambda_timeout
environment {
variables = {
TABLE_NAME = aws_dynamodb_table.tasks.name
ENVIRONMENT = var.environment
}
}
tracing_config {
mode = var.enable_xray ? "Active" : "PassThrough"
}
tags = {
Name = "${local.name_prefix}-get-tasks"
}
}
resource "aws_lambda_function" "update_task" {
filename = data.archive_file.update_task.output_path
function_name = "${local.name_prefix}-update-task"
role = aws_iam_role.lambda.arn
handler = "index.handler"
source_code_hash = data.archive_file.update_task.output_base64sha256
runtime = var.lambda_runtime
memory_size = var.lambda_memory
timeout = var.lambda_timeout
environment {
variables = {
TABLE_NAME = aws_dynamodb_table.tasks.name
ENVIRONMENT = var.environment
}
}
tracing_config {
mode = var.enable_xray ? "Active" : "PassThrough"
}
tags = {
Name = "${local.name_prefix}-update-task"
}
}
resource "aws_lambda_function" "delete_task" {
filename = data.archive_file.delete_task.output_path
function_name = "${local.name_prefix}-delete-task"
role = aws_iam_role.lambda.arn
handler = "index.handler"
source_code_hash = data.archive_file.delete_task.output_base64sha256
runtime = var.lambda_runtime
memory_size = var.lambda_memory
timeout = var.lambda_timeout
environment {
variables = {
TABLE_NAME = aws_dynamodb_table.tasks.name
ENVIRONMENT = var.environment
}
}
tracing_config {
mode = var.enable_xray ? "Active" : "PassThrough"
}
tags = {
Name = "${local.name_prefix}-delete-task"
}
}
# CloudWatch Log Groups
resource "aws_cloudwatch_log_group" "lambda_logs" {
for_each = toset(["create-task", "get-tasks", "update-task", "delete-task"])
name = "/aws/lambda/${local.name_prefix}-${each.key}"
retention_in_days = 14
}
api-gateway.tf
# API Gateway REST API
resource "aws_api_gateway_rest_api" "main" {
name = "${local.name_prefix}-api"
description = "Task Management API"
endpoint_configuration {
types = ["REGIONAL"]
}
tags = {
Name = "${local.name_prefix}-api"
}
}
# Authorizer Cognito
resource "aws_api_gateway_authorizer" "cognito" {
name = "${local.name_prefix}-cognito-authorizer"
rest_api_id = aws_api_gateway_rest_api.main.id
type = "COGNITO_USER_POOLS"
provider_arns = [aws_cognito_user_pool.main.arn]
}
# Resource /tasks
resource "aws_api_gateway_resource" "tasks" {
rest_api_id = aws_api_gateway_rest_api.main.id
parent_id = aws_api_gateway_rest_api.main.root_resource_id
path_part = "tasks"
}
# Resource /tasks/{taskId}
resource "aws_api_gateway_resource" "task" {
rest_api_id = aws_api_gateway_rest_api.main.id
parent_id = aws_api_gateway_resource.tasks.id
path_part = "{taskId}"
}
# POST /tasks - Create Task
resource "aws_api_gateway_method" "create_task" {
rest_api_id = aws_api_gateway_rest_api.main.id
resource_id = aws_api_gateway_resource.tasks.id
http_method = "POST"
authorization = "COGNITO_USER_POOLS"
authorizer_id = aws_api_gateway_authorizer.cognito.id
}
resource "aws_api_gateway_integration" "create_task" {
rest_api_id = aws_api_gateway_rest_api.main.id
resource_id = aws_api_gateway_resource.tasks.id
http_method = aws_api_gateway_method.create_task.http_method
integration_http_method = "POST"
type = "AWS_PROXY"
uri = aws_lambda_function.create_task.invoke_arn
}
# GET /tasks - Get Tasks
resource "aws_api_gateway_method" "get_tasks" {
rest_api_id = aws_api_gateway_rest_api.main.id
resource_id = aws_api_gateway_resource.tasks.id
http_method = "GET"
authorization = "COGNITO_USER_POOLS"
authorizer_id = aws_api_gateway_authorizer.cognito.id
}
resource "aws_api_gateway_integration" "get_tasks" {
rest_api_id = aws_api_gateway_rest_api.main.id
resource_id = aws_api_gateway_resource.tasks.id
http_method = aws_api_gateway_method.get_tasks.http_method
integration_http_method = "POST"
type = "AWS_PROXY"
uri = aws_lambda_function.get_tasks.invoke_arn
}
# PUT /tasks/{taskId} - Update Task
resource "aws_api_gateway_method" "update_task" {
rest_api_id = aws_api_gateway_rest_api.main.id
resource_id = aws_api_gateway_resource.task.id
http_method = "PUT"
authorization = "COGNITO_USER_POOLS"
authorizer_id = aws_api_gateway_authorizer.cognito.id
}
resource "aws_api_gateway_integration" "update_task" {
rest_api_id = aws_api_gateway_rest_api.main.id
resource_id = aws_api_gateway_resource.task.id
http_method = aws_api_gateway_method.update_task.http_method
integration_http_method = "POST"
type = "AWS_PROXY"
uri = aws_lambda_function.update_task.invoke_arn
}
# DELETE /tasks/{taskId} - Delete Task
resource "aws_api_gateway_method" "delete_task" {
rest_api_id = aws_api_gateway_rest_api.main.id
resource_id = aws_api_gateway_resource.task.id
http_method = "DELETE"
authorization = "COGNITO_USER_POOLS"
authorizer_id = aws_api_gateway_authorizer.cognito.id
}
resource "aws_api_gateway_integration" "delete_task" {
rest_api_id = aws_api_gateway_rest_api.main.id
resource_id = aws_api_gateway_resource.task.id
http_method = aws_api_gateway_method.delete_task.http_method
integration_http_method = "POST"
type = "AWS_PROXY"
uri = aws_lambda_function.delete_task.invoke_arn
}
# Lambda Permissions pour API Gateway
resource "aws_lambda_permission" "api_gateway" {
for_each = {
create = aws_lambda_function.create_task.function_name
get = aws_lambda_function.get_tasks.function_name
update = aws_lambda_function.update_task.function_name
delete = aws_lambda_function.delete_task.function_name
}
statement_id = "AllowAPIGatewayInvoke"
action = "lambda:InvokeFunction"
function_name = each.value
principal = "apigateway.amazonaws.com"
source_arn = "${aws_api_gateway_rest_api.main.execution_arn}/*"
}
# Deployment
resource "aws_api_gateway_deployment" "main" {
rest_api_id = aws_api_gateway_rest_api.main.id
depends_on = [
aws_api_gateway_integration.create_task,
aws_api_gateway_integration.get_tasks,
aws_api_gateway_integration.update_task,
aws_api_gateway_integration.delete_task
]
lifecycle {
create_before_destroy = true
}
}
# Stage
resource "aws_api_gateway_stage" "main" {
deployment_id = aws_api_gateway_deployment.main.id
rest_api_id = aws_api_gateway_rest_api.main.id
stage_name = var.environment
cache_cluster_enabled = var.enable_api_cache
cache_cluster_size = var.enable_api_cache ? var.api_cache_size : null
xray_tracing_enabled = var.enable_xray
access_log_settings {
destination_arn = aws_cloudwatch_log_group.api_logs.arn
format = jsonencode({
requestId = "$context.requestId"
ip = "$context.identity.sourceIp"
caller = "$context.identity.caller"
user = "$context.identity.user"
requestTime = "$context.requestTime"
httpMethod = "$context.httpMethod"
resourcePath = "$context.resourcePath"
status = "$context.status"
protocol = "$context.protocol"
responseLength = "$context.responseLength"
})
}
tags = {
Name = "${local.name_prefix}-${var.environment}"
}
}
resource "aws_cloudwatch_log_group" "api_logs" {
name = "/aws/api-gateway/${local.name_prefix}"
retention_in_days = 14
}
outputs.tf
output "api_endpoint" {
description = "API Gateway endpoint URL"
value = aws_api_gateway_stage.main.invoke_url
}
output "cognito_user_pool_id" {
description = "Cognito User Pool ID"
value = aws_cognito_user_pool.main.id
}
output "cognito_client_id_web" {
description = "Cognito Web Client ID"
value = aws_cognito_user_pool_client.web.id
}
output "cognito_client_id_mobile" {
description = "Cognito Mobile Client ID"
value = aws_cognito_user_pool_client.mobile.id
}
output "dynamodb_table_name" {
description = "DynamoDB table name"
value = aws_dynamodb_table.tasks.name
}
Code Lambda (Exemple)
lambda-code/create_task/index.py
import json
import boto3
import uuid
from datetime import datetime
import os
dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table(os.environ['TABLE_NAME'])
def handler(event, context):
try:
# Récupérer l'userId depuis le token Cognito
user_id = event['requestContext']['authorizer']['claims']['sub']
# Parser le body
body = json.loads(event['body'])
# Créer la tâche
task_id = str(uuid.uuid4())
now = datetime.utcnow().isoformat()
item = {
'userId': user_id,
'taskId': task_id,
'title': body['title'],
'description': body.get('description', ''),
'status': body.get('status', 'pending'),
'priority': body.get('priority', 'medium'),
'dueDate': body.get('dueDate', ''),
'createdAt': now,
'updatedAt': now
}
table.put_item(Item=item)
return {
'statusCode': 201,
'headers': {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
},
'body': json.dumps({
'message': 'Task created successfully',
'task': item
})
}
except Exception as e:
return {
'statusCode': 500,
'headers': {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
},
'body': json.dumps({
'error': str(e)
})
}
Coûts estimés
| Ressource | Coût mensuel |
|---|---|
| API Gateway | ~$3.50/million requêtes |
| Lambda | ~$0.20/million invocations |
| DynamoDB (PAY_PER_REQUEST) | ~$1.25/million requêtes |
| Cognito | Gratuit jusqu'à 50k MAU |
| Total (100k req/mois) | ~$1-5/mois |