Já pensou em como seria prático e seguro baixar arquivos diretamente do seu S3? Este artigo mostra como criar uma solução serverless usando serviços da AWS, como API Gateway, funções Lambda e S3 buckets, para permitir o download de arquivos do S3 usando URLs pré-assinadas. E o melhor: com rastreamento OpenTelemetry, você terá uma visão detalhada de cada etapa do processo, desde o API Gateway até o S3.
Este projeto oferece um método eficiente e seguro para baixar arquivos diretamente de um bucket S3, rastreando cada interação desde o API Gateway, passando pelo Lambda, até o próprio S3. A arquitetura, construída com CloudFormation, garante que todos os recursos funcionem em conjunto de forma otimizada.
A ideia é criar uma arquitetura serverless que permita aos usuários baixar arquivos de um bucket S3 de forma segura através de um endpoint do API Gateway. Para garantir a segurança, o API Gateway é protegido por um Lambda Authorizer, processando apenas as requisições autorizadas. Além disso, o rastreamento OpenTelemetry oferece uma visão detalhada do ciclo de vida de cada requisição, desde o API Gateway até as funções Lambda e as interações com o bucket S3.
Para que as funções Lambda funcionem corretamente, o projeto utiliza o AWS Distro for OpenTelemetry (ADOT) Lambda layer, que possibilita o rastreamento. Mais informações sobre como configurar essa layer podem ser encontradas aqui.
Componentes Principais da Arquitetura Serverless
Os componentes principais deste projeto são:
- API Gateway: Serve como ponto de entrada para as requisições da API, protegido com um Lambda Authorizer.
- Funções Lambda: Responsáveis pela lógica de negócios, incluindo:
- Autenticação de requisições via Lambda Authorizer.
- Geração de URLs pré-assinadas para o download de arquivos do S3.
- Tratamento de erros, como recursos incorretos do API Gateway ou nomes de arquivos inválidos.
- S3 Bucket: Armazena os arquivos disponíveis para download.
- OpenTelemetry Tracing: Rastreia e registra o fluxo de requisições para otimização de performance e debugging.
No template CloudFormation (infrastructure/root.yaml
), a função Lambda principal e a configuração da função IAM são definidas da seguinte forma:
Parameters:
S3BucketName:
Type: String
Default: 'store-files-20241010'
StageName:
Type: String
Default: 'dev'
OtelLambdaLayerArn:
Type: String
Default: 'arn:aws:lambda:eu-central-1:901920570463:layer:aws-otel-python-amd64-ver-1-25-0:1'
Resources:
MainLambdaFunction:
DependsOn:
- S3Bucket
Type: AWS::Lambda::Function
Properties:
FunctionName: MainLambdaFunction
Description: Makes requests to the S3 bucket
Runtime: python3.12
Handler: index.lambda_handler
Role: !GetAtt MainLambdaExecutionRole.Arn
Timeout: 30
MemorySize: 512
Environment:
Variables:
S3_BUCKET_NAME: !Ref S3BucketName
OTEL_PROPAGATORS: xray
AWS_LAMBDA_EXEC_WRAPPER: /opt/otel-instrument
TracingConfig:
Mode: Active
Layers:
- !Ref OtelLambdaLayerArn
Code:
ZipFile: |
import json
import boto3
import os
import logging
from botocore.exceptions import ClientError
from opentelemetry import trace
# Initialize logging
logger = logging.getLogger()
logger.setLevel(logging.INFO)
# OpenTelemetry Tracing
tracer = trace.get_tracer(__name__)
# Initialize the S3 client
s3_client = boto3.client('s3')
def lambda_handler(event, context):
# Start tracing for the request
with tracer.start_as_current_span("main-handler-span") as span:
# Log the entire event received
logger.info('Received event: %s', json.dumps(event))
# Extract and log routeKey, path, and method
route_key = event.get("routeKey")
path = event.get("path")
http_method = event.get("httpMethod")
logger.info('Route Key: %s', route_key)
logger.info('HTTP Method: %s', http_method)
logger.info('Path: %s', path)
# Add tracing attributes
span.set_attribute("routeKey", route_key)
span.set_attribute("http.method", http_method)
span.set_attribute("http.path", path)
# Handle routes based on HTTP method and path
if route_key == "GET /list" or (http_method == "GET" and path == "/list"):
logger.info("Handling /list request")
return list_objects(span)
elif route_key == "GET /download" or (http_method == "GET" and path == "/download"):
logger.info("Handling /download request")
return generate_presigned_url(event, span)
else:
# Return a specific error message for invalid routes
logger.error("Invalid route: %s", route_key)
return {
'statusCode': 404,
'body': json.dumps({"error": "Resource name is incorrect."})
}
def list_objects(span):
bucket_name = os.environ.get('S3_BUCKET_NAME')
try:
logger.info("Listing objects in bucket: %s", bucket_name)
response = s3_client.list_objects_v2(Bucket=bucket_name)
if 'Contents' not in response:
logger.info("No objects found in bucket.")
return {
'statusCode': 200,
'body': json.dumps({"objects": []})
}
object_keys = [{'File': obj['Key']} for obj in response['Contents']]
logger.info("Found %d objects in bucket.", len(object_keys))
span.set_attribute("s3.object_count", len(object_keys))
return {
'statusCode': 200,
'body': json.dumps({"objects": object_keys})
}
except ClientError as e:
logger.error("Error listing objects in the bucket: %s", e)
return {
'statusCode': 500,
'body': json.dumps({"error": "Error listing objects in the bucket."})
}
def generate_presigned_url(event, span):
bucket_name = os.environ.get('S3_BUCKET_NAME')
query_string_params = event.get("queryStringParameters")
if not query_string_params:
logger.error("Query string parameters are missing or request is incorrect.")
return {
'statusCode': 400,
'body': json.dumps({"error": "Request is incorrect. Please provide valid query parameters."})
}
object_key = query_string_params.get("objectKey")
logger.info("Bucket name: %s", bucket_name)
logger.info("Requested object key: %s", object_key)
if not object_key:
logger.error("objectKey is missing in queryStringParameters.")
return {
'statusCode': 400,
'body': json.dumps({"error": "objectKey is required."})
}
# Check if the object exists
try:
logger.info("Checking if object %s exists in bucket %s", object_key, bucket_name)
s3_client.head_object(Bucket=bucket_name, Key=object_key)
except ClientError as e:
if e.response['Error']['Code'] == '404':
logger.error("Object %s does not exist in bucket %s", object_key, bucket_name)
return {
'statusCode': 404,
'body': json.dumps({"error": "Object name is incorrect or object doesn't exist."})
}
else:
logger.error("Error checking object existence in bucket: %s", e)
return {
'statusCode': 500,
'body': json.dumps({"error": "Error checking object existence."})
}
# Generate presigned URL if the object exists
try:
logger.info("Generating presigned URL for object: %s", object_key)
presigned_url = s3_client.generate_presigned_url(
'get_object',
Params={'Bucket': bucket_name, 'Key': object_key},
ExpiresIn=900
)
logger.info("Generated presigned URL: %s", presigned_url)
span.set_attribute("s3.object_key", object_key)
return {
'statusCode': 200,
'body': json.dumps({"url": presigned_url})
}
except ClientError as e:
logger.error("Error generating presigned URL for object %s: %s", object_key, e)
return {
'statusCode': 500,
'body': json.dumps({"error": "Error generating presigned URL."})
}
MainLambdaExecutionRole:
Type: AWS::IAM::Role
Properties:
RoleName: MainLambdaExecutionRole
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service:
- lambda.amazonaws.com
Action:
- sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
Policies:
- PolicyName: S3BucketAccessPolicy
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- s3:ListBucket
- s3:GetObject
- s3:HeadObject
Resource:
- !Sub arn:${AWS::Partition}:s3:::${S3BucketName}/*
- !Sub arn:${AWS::Partition}:s3:::${S3BucketName}
- PolicyName: XRayAccessPolicy
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- xray:PutTraceSegments
- xray:PutTelemetryRecords
Resource: "*"
Pré-requisitos e Implantação
Antes de começar, certifique-se de que você tem:
- Uma conta AWS com permissões para criar e gerenciar recursos.
- O AWS CLI instalado na sua máquina local.
Para implantar, siga estes passos:
- Clone o repositório:
git clone https://gitlab.com/Andr1500/api-gateway-dowload-from-s3.git
- Crie um token de autorização no AWS Systems Manager Parameter Store:
aws ssm put-parameter --name "AuthorizationLambdaToken" --value "token_value_secret" --type "SecureString"
- Preencha todos os parâmetros necessários no template CloudFormation (
infrastructure/root.yaml
) e crie a stack CloudFormation:
aws cloudformation create-stack \
--stack-name api-gw-dowload-from-s3 \
--template-body file://infrastructure/root.yaml \
--capabilities CAPABILITY_NAMED_IAM --disable-rollback
- Recupere as URLs de invocação do Stage:
aws cloudformation describe-stacks --stack-name api-gw-dowload-from-s3 --query "Stacks[0].Outputs"
- Copie alguns arquivos para o bucket S3 para testes:
aws s3 cp images/apigw_lambda_s3.png s3://s3_bucket_name/apigw_lambda_s3.png
- Teste a API e baixe o arquivo com CURL.
Importante: Após a criação da infraestrutura, pode levar algum tempo para que o nome do bucket se propague pelas regiões AWS. Durante esse período, uma resposta “Temporary Redirect” pode ser recebida para requisições ao objeto com URL pré-assinada. Em alguns casos, mesmo com a região especificada no nome de domínio de origem, o download com a URL pré-assinada pode funcionar corretamente somente após um dia.
Para visualizar o mapa de rastreamento X-Ray e a linha do tempo dos segmentos, navegue até o AWS Console: CloudWatch -> X-Ray traces -> Traces. Na seção Traces, uma lista de IDs de rastreamento será exibida. Clicar em um ID de rastreamento revelará o mapa de rastreamento, a linha do tempo dos segmentos e os logs associados.
export APIGW_TOKEN='token_value_secret'
curl -X GET -H "Authorization: Bearer $APIGW_TOKEN" "https://api_id.execute-api.eu-central-1.amazonaws.com/dev/list"
curl -X GET -H "Authorization: Bearer $APIGW_TOKEN" "https://api_id.execute-api.eu-central-1.amazonaws.com/download?objectKey=apigw_lambda_s3.png"
curl -O "https://s3_bucket_name.s3.amazonaws.com/apigw_lambda_s3.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=ASIASAQWERTY123456%2Feu-central-1%2Fs3%2Faws4_request&X-Amz-Date=20241003T125740Z&X-Amz-Expires=600&X-Amz-SignedHeaders=host&X-Amz-Security-Token=token_value&X-Amz-Signature=signature_value"
- Limpe os recursos. Após os testes, exclua o token do Parameter Store e a stack CloudFormation:
aws ssm delete-parameter --name "AuthorizationLambdaToken"
aws cloudformation delete-stack --stack-name api-gw-dowload-from-s3
Este projeto oferece uma solução robusta para rastrear downloads de arquivos, utilizando serviços AWS como API Gateway, Lambda e S3, juntamente com o poder do rastreamento OpenTelemetry. O uso do CloudFormation para gerenciar a infraestrutura como código garante fácil implantação e repetibilidade.
Este conteúdo foi auxiliado por Inteligência Artificiado, mas escrito e revisado por um humano.
Via dev.to