Rastreamento OpenTelemetry para Downloads de Arquivos do S3

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:

  1. Clone o repositório:
    git clone https://gitlab.com/Andr1500/api-gateway-dowload-from-s3.git
  1. Crie um token de autorização no AWS Systems Manager Parameter Store:
    aws ssm put-parameter --name "AuthorizationLambdaToken" --value "token_value_secret" --type "SecureString"
  1. 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
  1. Recupere as URLs de invocação do Stage:
    aws cloudformation describe-stacks --stack-name api-gw-dowload-from-s3 --query "Stacks[0].Outputs"
  1. 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
  1. 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"
  1. 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

Leave a Comment

Exit mobile version