No meu artigo anterior, utilizámos um servidor proxy entre o Amazon Web Services (AWS) S3 e um front-end web para fazer upload de ficheiros. Desta vez, faremos isso sem a necessidade de usar um servidor. Para tal, usaremos o AWS Lambda e os URL pré-assinados do AWS S3.


Os URL pré-assinados concedem acesso público direto a objetos S3 privados, por um período de tempo limitado, protegidos pelas permissões de Gestão de Identidade e Acesso (IAM) do utilizador que gera o URL. Utilizaremos uma função do AWS Lambda para gerar URL pré-assinados do S3, liderados por um AWS API Gateway.

 

O diagrama abaixo ilustra os seguintes fluxos para fazer upload de um ficheiro através de um URL predefinido do S3:

  1. Obter o URL pré-assinado do S3;
  2. Carregar o ficheiro para o S3 através de um URL pré-assinado.
    .

 

S3 presigned URL 1

 

 

Geração de URL pré-assinados do S3

Para implementar a função Lambda que gera URL pré-assinados, escolhemos a versão mais recente do tempo de execução do Node.js (v20) e o AWS JS SDK v3. Aqui está o código da função:


import * as AWS from "@aws-sdk/client-s3";
import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";

const s3Configuration = {
    region: process.env.AWS_REGION_NAME,
};
const client = new S3Client(s3Configuration);

export const handler = async (event, context) => {
  const key = event.queryStringParameters.key
  const command = new PutObjectCommand({ Bucket: process.env.BUCKET_NAME, Key: key });
  const uploadURL = await getSignedUrl(client, command, { expiresIn: process.env.URL_EXPIRATION_SECONDS });
  
  return {
    "statusCode": 200,
    "isBase64Encoded": false,
    "headers": {
      "Access-Control-Allow-Origin": "*"
    },
    "body": JSON.stringify({
      "uploadURL": uploadURL,
        "key": key
    })
  };
}

A implementação do código gera um objeto S3Client, especificando a região do bucket S3 para onde queremos fazer upload de objetos. Se não for especificada uma região, o SDK tentará, respetivamente, ir buscá-la ao ambiente de execução da função Lambda (AWS_REGION), ou aos ficheiros de configuração partilhados.

 

Uma vez que as invocações da nossa função Lambda serão acionadas pelo AWS API Gateway, podemos esperar que a estrutura do objeto JSON do evento contenha a propriedade queryStringParameters, onde podemos passar um número arbitrário de propriedades. No nosso caso, esperamos um parâmetro de query “key” que corresponda à chave S3 do objeto a ser armazenado no bucket através do URL pré-assinado que será gerado.

 

De seguida, a função handler configura um PutObjectCommand para o bucket e a chave do objeto em questão, que é passado como parâmetro de entrada para a função getSignedUrl do SDK, juntamente com o objeto S3Client e um objeto de configuração que contém o tempo de expiração do URL em segundos. Por razões de segurança, o tempo de expiração deve ser o mais curto possível, para reduzir o risco de acesso não autorizado aos nossos recursos.

 

Por fim, a função de handler devolve um objeto JSON que inclui o seguinte:

  • statusCode: o código de estado de sucesso HTTP;
  • isBase64Encoded: um sinalizador que indica se o corpo da resposta está codificado com base64. Neste caso, não queremos que seja codificado em base64, uma vez que não estamos a transmitir dados binários;
  • headers: cabeçalhos adicionais para permitir a partilha de recursos entre origens (CORS) para aplicações web que chamam o nosso endpoint API Gateway, que, por sua vez, invoca esta função Lambda.

A função handler deve sempre devolver uma Promise ou utilizar a função de retorno de chamada, caso contrário, obtemos um erro HTTP 502 do API Gateway, com a mensagem “Internal server error”. Nos registos do API Gateway, também veremos o erro: “Execution failed due to configuration error: Malformed Lambda proxy response”.

 

Nota: o handler da função Lambda deve ter uma extensão “.mjs” para que seja designado como um módulo ES. Alternativamente, um ficheiro package.json deve ser configurado com o tipo “module”. Dessa forma, podemos usar a instrução de import para importar outros módulos ES, por exemplo, os módulos do AWS S3 SDK. Caso contrário, se o ficheiro tiver uma extensão “.js”, mas nenhum tipo de “module” estiver configurado num package.json, receberemos o erro “SyntaxError: Cannot use import statement outside a module”, ao importar módulos com a instrução de import ES.

 

Configuração de variáveis de ambiente

Para um código mais limpo, o código da função Lambda usa variáveis de ambiente para o nome da região, o nome do bucket e o tempo de expiração do URL em segundos. Para adicionar ou editar variáveis de ambiente para uma função Lambda, podemos fazê-lo no separador Configuration, seguido de Environment variables localizado no menu lateral, como se pode ver na imagem abaixo:

 

S3 presigned URL 2

 

As variáveis de ambiente adicionadas/editadas ficam imediatamente disponíveis para o processo Node da função e podem ser lidas através do objeto process.env do Node, como, por exemplo, process.env.BUCKET_NAME.

 

Nota: é aconselhável gerir centralmente propriedades comuns de aplicações através de serviços como o AWS System Manager (SSM) Parameter Store, especialmente para dados confidenciais como, por exemplo, credenciais.

 

Configuração de permissões

Os URL pré-assinados gerados herdam as permissões da função IAM da função Lambda. Se a nossa função Lambda não tiver o S3 com as permissões de escrita necessárias associadas à sua função IAM, os URL pré-assinados emitidos não poderão fazer upload de ficheiros para o bucket S3 em questão, em nome dos clientes, e obteremos um erro semelhante a este:

 

<Error>
 <Code>AccessDenied</Code>
 <Message>Access Denied</Message>
 <RequestId>ASDGssffscSDQ4POL</RequestId>
<HostId>OLsT4/9tYWsdssyo0dxtFa7sdsdsKhdPqsdsd4L+9CmiKP2tFyGsdsL8Pr0E0rkDgzHsddsVjdsdsdwc=</HostId>
</Error>

 

Vamos seguir o Princípio do Privilégio Mínimo (Principle of Least Privilege – PoLP) da cibersegurança e adicionar uma política inline que só permite a operação PutObject S3. Vamos para o separador Configuration da página lambda, selecionamos Permissions no menu lateral e, de seguida, em Execution role, pressionamos o link do nome da função para abrir a página da função IAM:

 

S3 presigned URL 3

 

Na página da função IAM da função lambda, no separador Permissions, premimos o botão Add permissions e selecionamos Create inline policy:

 

S3 presigned URL 4

 

Na página create policy, selecionamos JSON e adicionamos a seguinte política com o ARN do bucket S3:

 

S3 presigned URL 5

 

De seguida, adicionamos o nome da política e premimos o botão Create policy:

 

S3 presigned URL 6

 

Configuração do API Gateway

Para expor a nossa função Lambda a aplicações front-end, vamos utilizar o AWS API Gateway. Uma maneira de fazer isso é através do botão Add trigger, localizado no diagrama Lambda da nossa função:

 

S3 presigned URL 7

 

Na página Add trigger, ilustrada na imagem abaixo, selecionamos API Gateway como fonte, seguindo para “Create a new API” com API HTTP como o tipo de API. O API HTTP é um tipo de API mais leve e de baixa latência, sendo mais do que suficiente para as nossas necessidades. Por uma questão de brevidade e apenas para fins de demonstração, vamos selecionar “Open” como segurança, ou seja, qualquer pessoa pode aceder ao endpoint da API – note-se que, especialmente num ambiente de produção, é altamente recomendável que a API seja protegida com, por exemplo, AWS Cognito User Pools ou Lambda Authorizers.

 

S3 presigned URL 8

 

Uma vez adicionado o trigger do API Gateway, este fica visível no diagrama Lambda e o respetivo endpoint fica visível no separador Configuration, em Triggers, como se segue:

 

S3 presigned URL 9

 

Na secção seguinte, utilizaremos este URL do endpoint da API para obter URL pré-assinados do S3.

 

 

Upload de um ficheiro com um URL pré-assinado do S3

Para obtermos o URL pré-assinado do S3 para upload de um ficheiro, chamaremos o endpoint do AWS API Gateway que acionará uma chamada para a função Lambda criada anteriormente, e usaremos o Postman para isso.

 

Na imagem que se segue, podemos ver uma solicitação GET e uma resposta bem-sucedida para o endpoint mencionado anteriormente. O corpo da resposta JSON contém a propriedade uploadURL com o URL pré-assinado como seu valor.

 

S3 presigned URL 10

 

Finalmente, vamos criar um pedido PUT no Postman e utilizar o URL pré-assinado que pode ser copiado da resposta do pedido anterior. No separador Body do pedido, devemos selecionar “binary” e escolher um ficheiro do nosso disco local que queremos carregar para o S3 através do URL pré-assinado, conforme ilustrado na imagem abaixo. Depois de premirmos o botão Send, devemos ver uma resposta bem-sucedida, com um corpo vazio e o estado HTTP 200:

 

S3 presigned URL 11

 

Nota: para ficheiros com mais de 100 MB, recomenda-se o upload de várias partes (multipart upload).

 

 

Conclusão

Os URL pré-assinados do AWS S3 oferecem uma forma direta de fazer upload de ficheiros de um modo mais escalável, eficiente e fácil quando comparado com a utilização de um proxy de upload de servidor. Pode ajudar a reduzir custos operacionais, reduzindo a largura de banda e a carga de processamento nos servidores e, assim, aumentando a escalabilidade, uma vez que estes não precisam de lidar com o tráfego de upload de ficheiros.

 

Ao mesmo tempo, reduz a latência do pedido de upload de ficheiros e permite o carregamento de ficheiros de grandes dimensões, contornando os limites de carga útil do servidor de middleware, como, por exemplo, o limite de 10 MB do AWS API Gateway.

 

No entanto, como em tudo o resto, devem ser consideradas algumas desvantagens ao escolher URL pré-assinados do AWS S3:

  • Se um utilizador mal-intencionado obtiver o URL pré-assinado, poderá fazer upload de ficheiros para o bucket até que o URL expire. A definição de um tempo de expiração de URL curto ajuda certamente a mitigar esse risco. Os riscos também podem ser mais atenuados através de, por exemplo, mecanismos para limitar o número de vezes que um URL é usado;
  • Ao evitar a utilização de servidores, não é possível adicionar facilmente middleware personalizado ou etapas de processamento ao fluxo de upload de ficheiros, como a validação de formatos ou a verificação de vírus;
  • A observabilidade e o controlo do processo de upload podem também ser reduzidos. A monitorização e o registo estão limitados ao que o S3 e o CloudWatch fornecem, o que pode não ser tão abrangente como o que poderia ser conseguido com o processamento do lado do servidor. Manter um registo de auditoria detalhado dos uploads de ficheiros pode ser mais difícil, uma vez que o tráfego de carregamento não passa pelos nossos servidores.

 

 Lê aqui o artigo sobre Multipart Upload com o Amazon S3.

Partilha este artigo