Skip to content

Desenvolvendo a Infraestrutura

📝 Sobre o Projeto

O Projeto a seguir visa aplicar conceitos de Computação em Nuvem (Cloud Computing) por meio da plataforma de serviços de Computação em Nuvem AWS (Amazon Web Services). A ideia é subir uma aplicação sem servidor na AWS utilizando o S3, Lambda, API Gateway e o CloudWatch colocando em prática os conceitos de IaaC (Infrastructure as a Code) por meio do Terraform. O diagrama visual da aplicação pode ser conferido a seguir:

Diagrama da Aplicação

Desenvolvendo a infraestrutura

1. Pré-requisitos

  • Para rodar nossa infraestrutura, estamos utilizando o Ubuntu 22.04.2 LTS (o qual já estava instalado no nosso Windows). A infra pode funcionar em outras versões, porém não há garantia de funcionamento. Assim sendo, indicamos o uso da versão supracitada para testar nossa aplicação (e até mesmo para criar e rodar a sua própria).

ubuntu

2. Instalação do Terraform

Terraform image

A primeira etapa para desenvolvermos essa aplicação é instalar o Terraform na máquina. O Terraform é uma ferramenta de gerenciamento de infraestrutura como código (IaC) desenvolvida pela HashiCorp. Ele permite que os usuários definam, configurem e provisionem infraestruturas de forma automatizada e reprodutível, usando uma linguagem declarativa e uma sintaxe simples. Com o Terraform, é possível criar e gerenciar recursos em diferentes provedores de nuvem, como AWS, Google Cloud, Azure e outros, bem como em plataformas de infraestrutura, como Kubernetes, Docker e OpenStack. Em resumo, o Terraform é uma ferramenta importante para automatizar e gerenciar infraestruturas de nuvem e outras plataformas de infraestrutura, tornando a gestão de infraestrutura escalável, segura e repetível. Sim, é o Terraform quem irá nos auxiliar a não ter que ficar fazendo tudo manualmente no dashboard da AWS, como acabamos de fazer na página anterior.

Caso você não possua o Terraform no seu computador, é necessário baixar e instalar de acordo com o tutorial presente neste link (Windows) ou diretamente neste link (Ubuntu/Linux - Recomendado).

3. Utilização

Após a instalação do Terraform na máquina, já é possível rodar a infraestrutura desenvolvida ou criar sua própria infraestrutura com base em tudo que está sendo apresentado aqui.

O primeiro passo é clonar este repositório em uma pasta dentro do seu computador. Caso não saiba como clonar um repositório na sua máquina local, acesse o tutorial presente neste link ou faça o download do respositório e descompacte o arquivo .zip no local desejado.

Dica

Caso você desejar criar a infraestrutura do zero, segui e sugiro a seguinte estrutura de pastas:

Cloud-Project
└───hello
│   |---function.js
└───s3
|   │---function.js
│───Terraform
│   |---api-gateway.tf
│   |
│   |---hello-api-gateway.tf
│   |
│   |---hello-lamba.tf
│   |
│   |---lambda-s3-bucket.tf
│   |
│   |---provider.tf
│   |
│   |---s3-lambda.tf
│   |
│   |---test-bucket.tf
│   |
│   |---terraform.sh

Independentemente se você escolheu clonar o repositório com a infraestrutura original ou se tiver escolhido criar do zero, lembre-se que tudo deve ser feito dentro do prompt de comando do Ubuntu 22.04.2 LTS caso deseje chegar nos mesmos resultados apresentados aqui sem grandes riscos de problemas.


Credenciais AWS no Terraform

Antes de darmos início, é necessário cadastrar suas credenciais AWS na sua máquina. Temos várias formas de fazer isso, porém iremos optar pela alternativa que eu considero mais segura para quem está iniciando na AWS (incluindo eu mesmo): cadastrar diretamente via terminal. Sim, pode haver várias outras formas de fazer isso, então se você considera alguma outra melhor, fique à vontade.

Você deve possuir um .csv com a chave de acesso (Secret Key) e a chave secreta (Secret Access Key) de acesso da sua conta AWS, precisaremos dessas informações agora.

Aviso

A partir de agora, todos os comandos que utilizarmos ou alterações realizadas via prompt de comando deve ser feitas dentro da pasta que designamos para trabalhar neste projeto. Eu optei por trabalhar dentro da raiz da minha máquina, ou seja, dentro do meu diretório, estou trabalhando na seguinte pasta:

root@Lister:/mnt/c/Computacao_em_Nuvem/testando-o-projeto-01/Cloud-Project

Dica

Para manipular arquivos dentro do terminal do Ubuntu, utilize os comandos:

cd → Para navegar entre as pastas dentro do seu computador;

ls → para visualizar os arquivos dentro das pastas;

Exemplo de uso:

cd /mnt/c → Entrei na raiz do meu computador;

cd .. → Sai da raiz e "voltei um caminho", ou seja, agora estou no diretório "/mnt";

code . → Abre toda sua pasta dentro do VS Code (isso sempre me ajuda muito).

Dentro do terminal Ubuntu, insira os comandos:

aws configure

Após o comando acima, serão solicitadas as suas chaves de acesso. Coloque-as no terminal como solicitado e bora trabalhar!

Rodando a aplicação (se você desejar APENAS rodar a infra já criada)

Caso você desejar criar a infra (tal como sugerido e explicado detalhadamente neste handout), vá para a aba "Criando a infraestrutura do zero" e continue o handout. Caso desejar apenas rodá-la, entre na pasta que você clonou a infra e, dentro do diretório "Cloud-Project/terraform" rode os seguintes comandos:

terraform init

TERRAFORM INIT

E, em seguida, rode:

terraform plan

The terraform plan command creates an execution plan, which lets you preview the changes that Terraform plans to make to your infrastructure. (Texto extraído do site da Hashicorp)

O comando terraform plan cria um plano de execução, que permite visualizar as alterações que o Terraform planeja fazer em sua infraestrutura. (Texto traduzido do site da Hashicorp)

E, para finalizar, rode:

terraform apply

TERRAFORM INIT

Após tudo ter funcionado, a infra foi construída na AWS, agora é só testar!

Dica

Entre na sua dashboard da AWS, verifique todos os serviços criados e tente entender como tudo foi criado e o que está acontecendo entre os serviços (para isso, observar o diagrama apresentado no início do handout pode ajudar).

Para testar, digite no terminal:

curl -X POST \
-H "Content-Type: application/json" \
-d '{"name":"Insper"}' \
"https://<id>.execute-api.us-east-1.amazonaws.com/dev/hello"
          /\
          ||
Substitua <id> pelo seu id retornado no prompt de comando

Dica

Além do prompt de comando, teste também diretamente no seu navegador! Para fazer isso, basta substituir a url que foi retornada e passar algum parâmetro após "Name", da seguinte forma:

https:// SEU-ID-AQUI .execute-api.us-east-1.amazonaws.com/dev/hello + o parâmetro ?Name=Lister

Por exemplo:

https://2cmcnumb2l.execute-api.us-east-1.amazonaws.com/dev/hello?Name=Lister

Podemos também invocar essa função com o nome do bucket reebido no prompt de comando + nosso objeto para ver se o lambda conseguirá obter o objeto do bucket.

aws lambda invoke \
--region=us-east-1 \
--function-name=s3 \
--cli-binary-format raw-in-base64-out \
--payload '{"bucket":"test-<your>-<name>","object":"hello.json"}' \
response.json
                             /\
                             ||
      Substitua test-<your>-<name> pelo nome do seu bucket

Rode no terminal o seguinte comando:

cat response.json

  Se você receber como retorno Yeah, I am working from Insper, Avelinux :), parabéns, você concluiu sua aplicação e ela está funcionando!

Dica Visual

Se entrarmos no dashboard do CloudWatch novamente, conseguiremos ver os logs de acesso registrados para nossas solicitações.

Criando a infraestrutura do zero

Criando uma função Lambda no Terraform

O primeiro passo será criarmos uma função lambda que, futuramente, será integrada com o AWS API Gateway.

Mas o que é o "Lambda"?

A AWS Lambda é um serviço de computação sem servidor que permite executar código de forma escalável e sem a necessidade de gerenciar servidores. Ela é projetada para responder a eventos específicos, como alterações em um bucket do Amazon S3 ou atualizações em uma tabela do Amazon DynamoDB.

Inicialmente, começaremos com uma função simples baseada em NodeJS sem nenhuma dependência.

exports.handler = async (event) => {
  console.log("Event: ", event);
  let responseMessage = "Hello, World!";

  if (event.queryStringParameters && event.queryStringParameters["Name"]) {
    responseMessage = "Hello, " + event.queryStringParameters["Name"] + "!";
  }

  return response;
};

Ao invocar essa função com uma consulta de URL e com o parâmetro Name definido, ela retornará "Hello, Name!".

exports.handler = async (event) => {
  console.log("Event: ", event);
  let responseMessage = "Hello, World!";

  if (event.queryStringParameters && event.queryStringParameters["Name"]) {
    responseMessage = "Hello, " + event.queryStringParameters["Name"] + "!";
  }

+  if (event.httpMethod === "POST") {
+    const body = JSON.parse(event.body);
+    responseMessage = "Hello, " + body.name + "!";
+  }

+  const response = {
+    statusCode: 200,
+    headers: {
+      "Content-Type": "application/json",
+    },
+    body: JSON.stringify({
+      message: responseMessage,
+    }),
+  };

  return response;
};

Também será verificado o método HTTP GET e POST para que seja verificada a resposta padrão. Foi especificado o código de status '200', tipo de conteúdo e a mensagem para ser retornada ao chamador.

Criando o provider

Agora que nossa handler já está pronta, começaremos a trabalhar em alguns elementos do nosso Terraform. Criaremos os arquivos .tf dentro de uma pasta intitulada (por motivos intuitivos, claro) como "terraform".

Começaremos criando o arquivo "provider.tf", em que serão declaradas as restrições de versão (região da AWS, por exemplo) para os diferentes provedores AWS, versões de Teraform e afins.

Isso é feito apenas para que, caso alguém pegue esse projeto no futuro e rode em outra versão de Terraform ou com configurações diferentes das que foram aqui padronizadas, o projeto não funcione.

terraform {
    required_providers {
    aws = {
        source = "hashicorp/aws"
        version = "~> 4.21.0"
        }
    # Aqui dentro poderíamos também ter estabelecido outras restrições
    # de versão, mas optei por deixar o mais simples possível.
    }

required_version = "~> 1.0"
}

provider "aws" {
    region = "us-east-1" # Região Northern Virginia
}

Bucket do S3

Agora nós construiremos uma função com todas as dependências, empacotaremos como um arquivo zip para que, assim, consigamos subir num bucket do S3. Ou seja, quando criamos o lambda, apontamos para esse objeto de arquvo zip no bucket S3.

Como os nomes dos buckets do S3 devem ser únicos e exclusivos no mundo inteiro, podemos utilizar um gerador aleatório para nos ajudar a nomear nosso bucket S3.

1
2
3
4
resource "random_pet" "lambda_bucket_name" {
prefix = "lambda"
length = 2
}

Em seguida, vamos criar o próprio bucket do S3 com o nome gerado.

1
2
3
4
5
6
7
8
9
resource "random_pet" "lambda_bucket_name" {
  prefix = "lambda"
  length = 2
}

+resource "aws_s3_bucket" "lambda_bucket" {
+  bucket        = random_pet.lambda_bucket_name.id
+  force_destroy = true
+}

Por padrão, deixaremos todo o acesso público ao bucket bloqueado.

resource "random_pet" "lambda_bucket_name" {
  prefix = "lambda"
  length = 2
}

resource "aws_s3_bucket" "lambda_bucket" {
  bucket        = random_pet.lambda_bucket_name.id
  force_destroy = true
}

+resource "aws_s3_bucket_public_access_block" "lambda_bucket" {
+  bucket = aws_s3_bucket.lambda_bucket.id

+  block_public_acls       = true
+  block_public_policy     = true
+  ignore_public_acls      = true
+  restrict_public_buckets = true
+}

IAM e Policies

Agora criaremos o código Terraform do lambda. Lembrando que o lambda exigirá acesso a outros serviços da AWS (como o CloudWatch, para gravar logs) e, no nosso caso, concederemos acesso ao bucket do S3 para que seja possível a leitura de um arquivo.

Para isso, precisamos criar uma função do IAM e permitir que o lambda a use.

resource "aws_iam_role" "hello_lambda_exec" {
  name = "hello-lambda"

  assume_role_policy = <<POLICY
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "lambda.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
POLICY
}

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

Criando uma função Lambda

O próximo recurso será criar a função lambda, a qual chamaremos de "hello". Em seguida especificaremos o nome do intervalo onde armazenaremos todos os lambdas. E nosso key pointing irá apontar para um arquivo zip com uma função.

O hash do código-fonte foi adicionado para reimplementar a função caso seja alterado/atualizado algo no código-fonte. Se o hash do arquivo zip for diferente, a reimplantação do lambda será forçada.

resource "aws_iam_role" "hello_lambda_exec" {
  name = "hello-lambda"

  assume_role_policy = <<POLICY
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "lambda.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
POLICY
}

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

+resource "aws_lambda_function" "hello" {
+  function_name = "hello"

+  s3_bucket = aws_s3_bucket.lambda_bucket.id
+  s3_key    = aws_s3_object.lambda_hello.key

+  runtime = "nodejs16.x"
+  handler = "function.handler"

+  source_code_hash = data.archive_file.lambda_hello.output_base64sha256

+  role = aws_iam_role.hello_lambda_exec.arn
+}

Criando o CloudWatch

O CloudWatch é um serviço de monitoramento e observabilidade oferecido pela AWS. Ele permite que você colete e monitore métricas, registros e eventos de diferentes recursos e serviços da AWS, fornecendo insights valiosos sobre o desempenho, a saúde e a disponibilidade do seu ambiente de nuvem. Com o CloudWatch, você pode definir alarmes para acionar ações automáticas, criar painéis personalizados para visualizar dados em tempo real, além de acessar informações históricas para análise e solução de problemas. Ele é uma ferramenta fundamental para garantir o monitoramento proativo e a operação eficiente dos seus recursos na nuvem da AWS.

Para depurar, criamos um grupo de logs do CloudWatch que conseguisse armazenar todas as instruções e erros do console.log na função. Definimos a retenção para 30 dias, porém poderia ser uma quantidade maior ou menor também, a depender das intenções de quem está desenvolvendo a infraestrutura (além dessas decisões poderem afetar o custo de execução do lambda).

resource "aws_iam_role" "hello_lambda_exec" {
  name = "hello-lambda"

  assume_role_policy = <<POLICY
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "lambda.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
POLICY
}

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

resource "aws_lambda_function" "hello" {
  function_name = "hello"

  s3_bucket = aws_s3_bucket.lambda_bucket.id
  s3_key    = aws_s3_object.lambda_hello.key

  runtime = "nodejs16.x"
  handler = "function.handler"

  source_code_hash = data.archive_file.lambda_hello.output_base64sha256

  role = aws_iam_role.hello_lambda_exec.arn
}

+resource "aws_cloudwatch_log_group" "hello" {
+  name = "/aws/lambda/${aws_lambda_function.hello.function_name}"

+  retention_in_days = 30
+}

Em seguida, adicionaremos o recurso que empacota o lambda como um arquivo zip.

resource "aws_iam_role" "hello_lambda_exec" {
  name = "hello-lambda"

  assume_role_policy = <<POLICY
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "lambda.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
POLICY
}

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

resource "aws_lambda_function" "hello" {
  function_name = "hello"

  s3_bucket = aws_s3_bucket.lambda_bucket.id
  s3_key    = aws_s3_object.lambda_hello.key

  runtime = "nodejs16.x"
  handler = "function.handler"

  source_code_hash = data.archive_file.lambda_hello.output_base64sha256

  role = aws_iam_role.hello_lambda_exec.arn
}

resource "aws_cloudwatch_log_group" "hello" {
  name = "/aws/lambda/${aws_lambda_function.hello.function_name}"

  retention_in_days = 14
}

+data "archive_file" "lambda_hello" {
+  type = "zip"

+  source_dir  = "../${path.module}/hello"
+  output_path = "../${path.module}/hello.zip"
+}

Nosso último componente visa obter o arquivo zip e carregar no bucket do S3.

resource "aws_iam_role" "hello_lambda_exec" {
  name = "hello-lambda"

  assume_role_policy = <<POLICY
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "lambda.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
POLICY
}

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

resource "aws_lambda_function" "hello" {
  function_name = "hello"

  s3_bucket = aws_s3_bucket.lambda_bucket.id
  s3_key    = aws_s3_object.lambda_hello.key

  runtime = "nodejs16.x"
  handler = "function.handler"

  source_code_hash = data.archive_file.lambda_hello.output_base64sha256

  role = aws_iam_role.hello_lambda_exec.arn
}

resource "aws_cloudwatch_log_group" "hello" {
  name = "/aws/lambda/${aws_lambda_function.hello.function_name}"

  retention_in_days = 30
}

data "archive_file" "lambda_hello" {
  type = "zip"

  source_dir  = "../${path.module}/hello"
  output_path = "../${path.module}/hello.zip"
}

+resource "aws_s3_object" "lambda_hello" {
+  bucket = aws_s3_bucket.lambda_bucket.id

+  key    = "hello.zip"
+  source = data.archive_file.lambda_hello.output_path

+  etag = filemd5(data.archive_file.lambda_hello.output_path)
+}

Inicializando o Terraform

Com os passos feitos até aqui já conseguimos inicializar o terraform:

terraform init

terraforminit

Agora aplicamos as alterações:

terraform apply

terraforminit

⚠ Dica visual

Quando o terraform concluir suas etapas até aqui, podemos entrar no dashboard da AWS e encontrar, dentre outras coisas, um bucket S3 recém-criado com um nome definido por meio de um gerador de animais de estimação aleatório. terraforminit > terraforminit


Para abstrair:

Note que dentro do bucket são armazenadas funções lambdas dentro de um zip.

Quando entramos na dashboard da AWS CloudWatch também conseguimos ver o grupo de logs criado.

terraforminit

No dashboard do AWS Lambda conseguimos ver a função lambda empacotada como um zip.

terraforminit


Vamos agora invocar a função com o comando aws lambda invoke.

Lembre-se de especificar ou conferir se o nome da região, função e arquivo estão corretos para registrar a resposta da função.

aws lambda invoke --region=us-east-1 --function-name=hello response.json

terraforminit

Ao printarmos a resposta, é esperado um retorno "Olá, Avelino!"

cat response.json

terraforminit

Criando o API Gateway

A próxima estapa será criar o API Gateway e integrá-lo ao nosso lambda.

O API Gateway é um serviço da AWS que permite criar, publicar, proteger e gerenciar APIs (Interfaces de Programação de Aplicativos). Ele atua como um ponto de entrada para os aplicativos acessarem funcionalidades e dados, fornecendo recursos como autenticação, autorização, limitação de taxa e transformação de dados. O API Gateway simplifica o processo de criação de APIs, permitindo que você se concentre na lógica do aplicativo, enquanto ele gerencia aspectos como escalabilidade, segurança e monitoramento. Com o API Gateway, é possível criar APIs RESTful ou WebSocket para integrar e expor seus serviços e recursos de forma segura na nuvem da AWS.

resource "aws_apigatewayv2_api" "main" {
  name          = "main"
  protocol_type = "HTTP"
}

resource "aws_apigatewayv2_stage" "dev" {
  api_id = aws_apigatewayv2_api.main.id

  name        = "dev"
  auto_deploy = true

  access_log_settings {
    destination_arn = aws_cloudwatch_log_group.main_api_gw.arn

    format = jsonencode({
      requestId               = "$context.requestId"
      sourceIp                = "$context.identity.sourceIp"
      requestTime             = "$context.requestTime"
      protocol                = "$context.protocol"
      httpMethod              = "$context.httpMethod"
      resourcePath            = "$context.resourcePath"
      routeKey                = "$context.routeKey"
      status                  = "$context.status"
      responseLength          = "$context.responseLength"
      integrationErrorMessage = "$context.integrationErrorMessage"
      }
    )
  }
}

resource "aws_cloudwatch_log_group" "main_api_gw" {
  name = "/aws/api-gw/${aws_apigatewayv2_api.main.name}"

  retention_in_days = 30
}

Integrando o API Gateway com o Lambda

No próximo arquivo Terraform, integraremos o API Gateway com o hello lambda. Primeiramente apontaremos para o ID do API Gateway que acabamos de criar. Em seguida utilizaremos PROXYS AWS e solicitações POST para encaminhar solicitações do API Gateway para o Lambda

1
2
3
4
5
6
7
resource "aws_apigatewayv2_integration" "lambda_hello" {
api_id = aws_apigatewayv2_api.main.id

integration_uri = aws_lambda_function.hello.invoke_arn
integration_type = "AWS_PROXY"
integration_method = "POST"
}

Podemos especificar qual tipo de solicitações queremos passar para o lambda, por exemplo: GET ou POST, como abaixo:

resource "aws_apigatewayv2_integration" "lambda_hello" {
api_id = aws_apigatewayv2_api.main.id

integration_uri = aws_lambda_function.hello.invoke_arn
integration_type = "AWS_PROXY"
integration_method = "POST"
}

+resource "aws_apigatewayv2_route" "get_hello" {
+api_id = aws_apigatewayv2_api.main.id

+route_key = "GET /hello"
+target = "integrations/${aws_apigatewayv2_integration.lambda_hello.id}"
+}

+resource "aws_apigatewayv2_route" "post_hello" {
+api_id = aws_apigatewayv2_api.main.id

+route_key = "POST /hello"
+target = "integrations/${aws_apigatewayv2_integration.lambda_hello.id}"
+}

Note que em ambos os exemplos é necessário especificar um destino para ser o nosso lambda. Também precisamos conceder permissões ao API Gateway para invocar nossa função lambda:

resource "aws_apigatewayv2_integration" "lambda_hello" {
  api_id = aws_apigatewayv2_api.main.id

  integration_uri    = aws_lambda_function.hello.invoke_arn
  integration_type   = "AWS_PROXY"
  integration_method = "POST"
}

resource "aws_apigatewayv2_route" "get_hello" {
  api_id = aws_apigatewayv2_api.main.id

  route_key = "GET /hello"
  target    = "integrations/${aws_apigatewayv2_integration.lambda_hello.id}"
}

resource "aws_apigatewayv2_route" "post_hello" {
  api_id = aws_apigatewayv2_api.main.id

  route_key = "POST /hello"
  target    = "integrations/${aws_apigatewayv2_integration.lambda_hello.id}"
}

+resource "aws_lambda_permission" "api_gw" {
+  statement_id  = "AllowExecutionFromAPIGateway"
+  action        = "lambda:InvokeFunction"
+  function_name = aws_lambda_function.hello.function_name
+  principal     = "apigateway.amazonaws.com"

+  source_arn = "${aws_apigatewayv2_api.main.execution_arn}/*/*"
+}

Invocando o Lambda

Por fim, vamos imprimir no console o URL que podemos usar para invocar o lambda.

resource "aws_apigatewayv2_integration" "lambda_hello" {
  api_id = aws_apigatewayv2_api.main.id

  integration_uri    = aws_lambda_function.hello.invoke_arn
  integration_type   = "AWS_PROXY"
  integration_method = "POST"
}

resource "aws_apigatewayv2_route" "get_hello" {
  api_id = aws_apigatewayv2_api.main.id

  route_key = "GET /hello"
  target    = "integrations/${aws_apigatewayv2_integration.lambda_hello.id}"
}

resource "aws_apigatewayv2_route" "post_hello" {
  api_id = aws_apigatewayv2_api.main.id

  route_key = "POST /hello"
  target    = "integrations/${aws_apigatewayv2_integration.lambda_hello.id}"
}

resource "aws_lambda_permission" "api_gw" {
  statement_id  = "AllowExecutionFromAPIGateway"
  action        = "lambda:InvokeFunction"
  function_name = aws_lambda_function.hello.function_name
  principal     = "apigateway.amazonaws.com"

  source_arn = "${aws_apigatewayv2_api.main.execution_arn}/*/*"
}

+output "hello_base_url" {
+  value = aws_apigatewayv2_stage.dev.invoke_url
+}

No terminal, daremos um apply no terraform.

terraform apply

terraforminit

⚠ Dica visual

Se entrarmos no dashboard do API Gateway, podemos ver nosso estágio de desenvolvimento "dev" criado e, em "rotas", conseguimos encontrar os métodos GET e POST. > terraforminit > terraforminit

Até essa etapa, se você estiver seguindo a risca minha forma de fazer esse handout, é esperada que a estrutura do seu código esteja como na imagem abaixo:

terraforminit

Hora de testar

Vamos agora testar o método HTTP GET. A função deve analisá-lo e retornar a mensagem "Olá, + parâmetro de URL"

curl "https://<id>.execute-api.us-east-1.amazonaws.com/dev/hello?Name=InsperUniversity"
               /\
               ||
Substitua <id> pelo seu id retornado no prompt de comando

GET

Também testaremos o método POST. Nesse caso, fornecemos um objeto json para o terminal e veremos que funciona também.

curl -X POST \
-H "Content-Type: application/json" \
-d '{"name":"Insper"}' \
"https://<id>.execute-api.us-east-1.amazonaws.com/dev/hello"
          /\
          ||
Substitua <id> pelo seu id retornado no prompt de comando

POST

Dica

Além do prompt de comando, teste também diretamente no seu navegador! Para fazer isso, basta substituir a url que foi retornada e passar algum parâmetro após "Name", da seguinte forma:

https:// SEU-ID-AQUI .execute-api.us-east-1.amazonaws.com/dev/hello + o parâmetro ?Name=Lister

Por exemplo:

https://2cmcnumb2l.execute-api.us-east-1.amazonaws.com/dev/hello?Name=Lister

⚠ Dica visual

Se entrarmos no dashboard do CloudWatch, conseguiremos ver os logs de acesso registrados para nossas solicitações. > Logs_CloudWatch > Logs_CloudWatch

Criando função lambda com dependências externas e acesso ao bucket S3

Vamos agora criar outra função lambda com dependências externas e que garanta acesso para a leitura de um arquivo em um bucket S3.

Novamente, utilizaremos o random pet para nos auxiliar com um nome aleatório e único para nosso bucket do S3 e dicionaremos o prefixo "test" (o que também auxilia na identificação do bucket).

Para esse bucket também deixaremos o acesso público desativado.

resource "random_pet" "test_bucket_name" {
  prefix = "test"
  length = 2
}

resource "aws_s3_bucket" "test" {
  bucket        = random_pet.test_bucket_name.id
  force_destroy = true
}

resource "aws_s3_bucket_public_access_block" "test" {
  bucket = aws_s3_bucket.test.id

  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

Nós também podemos criar um objeto no S3 bucket utilizando terraform e o jsoncode build-in. Isso irá converter para um objeto json válido.

resource "random_pet" "test_bucket_name" {
  prefix = "test"
  length = 2
}

resource "aws_s3_bucket" "test" {
  bucket        = random_pet.test_bucket_name.id
  force_destroy = true
}

resource "aws_s3_bucket_public_access_block" "test" {
  bucket = aws_s3_bucket.test.id

  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

+resource "aws_s3_object" "test" {
+  bucket = aws_s3_bucket.test.id

+  key     = "hello.json"
+  content = jsonencode({ name = "S3" })
+}

+output "test_s3_bucket" {
+  value = random_pet.test_bucket_name.id
+}

Agora iremos criar uma nova função lambda em uma nova pasta s3.

Começaremos importando o aws-sdk e inicializando o objeto javascript.

Essa função irá nos retornar o conteúdo do objeto.

const aws = require("aws-sdk");

const s3 = new aws.S3({ apiVersion: "2006-03-01" });

exports.handler = async (event, context) => {
  console.log("Received event:", JSON.stringify(event, null, 2));

  const bucket = event.bucket;
  const object = event.object;
  const key = decodeURIComponent(object.replace(/\+/g, " "));

  const params = {
    Bucket: bucket,
    Key: key,
  };
  try {
    const { Body } = await s3.getObject(params).promise();
    const content = Body.toString("utf-8");
    return content + " Yeah, I am working, Avelinux :) !!";
  } catch (err) {
    console.log(err);
    const message = `Error getting object ${key} from bucket ${bucket}.`;
    console.log(message);
    throw new Error(message);
  }
};

Dentro do diretório da recém-criada pasta "s3", devemos inicializar o projeto nodejs com o seguinte comando:

npm init

Esse comando irá gerar arquivos package.json com dependências. Não há necessidade de preencher as informações solicitadas, basta teclar "enter" para cada info solicitada.

Logs_CloudWatch

Em seguida, iremos instalar o módulo aws-sdk:

npm install aws-sdk

Logs_CloudWatch

Agora retornaremos à pasta "Terraform" e iremos criar nossas políticas de acesso.

resource "aws_iam_role" "s3_lambda_exec" {
  name = "s3-lambda"

  assume_role_policy = <<POLICY
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "lambda.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
POLICY
}

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

resource "aws_iam_policy" "test_s3_bucket_access" {
  name        = "TestS3BucketAccess"

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = [
          "s3:GetObject", #Permitir obter um objeto do bucket
        ]
        Effect   = "Allow"
        Resource = "arn:aws:s3:::${aws_s3_bucket.test.id}/*"
      },
    ]
  })
}

# Política para acessar o novo bucket do s3:

resource "aws_iam_role_policy_attachment" "s3_lambda_test_s3_bucket_access" {
  role       = aws_iam_role.s3_lambda_exec.name
  policy_arn = aws_iam_policy.test_s3_bucket_access.arn
}

# Aqui teremos apoio na extração do aquivo zip da função

resource "aws_lambda_function" "s3" {
  function_name = "s3"

  s3_bucket = aws_s3_bucket.lambda_bucket.id
  s3_key    = aws_s3_object.lambda_s3.key

  runtime = "nodejs16.x"
  handler = "function.handler"

  source_code_hash = data.archive_file.lambda_s3.output_base64sha256

  role = aws_iam_role.s3_lambda_exec.arn
}

# Novo grupo de logs do CloudWatch para a função:

resource "aws_cloudwatch_log_group" "s3" {
  name = "/aws/lambda/${aws_lambda_function.s3.function_name}"

  retention_in_days = 14
}


# "Compacte a função e carregue o zip para o bucket s3:"

data "archive_file" "lambda_s3" {
  type = "zip"

  source_dir  = "../${path.module}/s3"
  output_path = "../${path.module}/s3.zip"
}

resource "aws_s3_object" "lambda_s3" {
  bucket = aws_s3_bucket.lambda_bucket.id

  key    = "s3.zip"
  source = data.archive_file.lambda_s3.output_path

  source_hash = filemd5(data.archive_file.lambda_s3.output_path)
}

Vamos agora deployar diretamente na máquina local. Para isso será necessário criar um simples script wrapper no Terraform:

1
2
3
4
5
6
7
8
9
#!/bin/sh

set -e

cd ../s3
npm ci

cd ../terraform
terraform apply

(No terminal, diretório /terraform) Vamos fazer o script se tornar executável:

chmod +x terraform.sh

Em seguida podemos rodar:

./terraform.sh

Outputs

Note que, ao rodarmos o comando acima, ele automaticamente rodará nosso terraform.sh que possui um "terraform apply", aplicando imediatamente as alterações que fizemos, sem que tenhamos que utilizar novamente o comando "terraform apply" no terminal.

No terminal receberemos de volta o nome do nosso recém-criado bucket do s3. Podemos invocar essa função s3 com o nome do bucket + nosso objeto para ver se o lambda conseguirá obter o objeto do bucket.

aws lambda invoke \
--region=us-east-1 \
--function-name=s3 \
--cli-binary-format raw-in-base64-out \
--payload '{"bucket":"test-<your>-<name>","object":"hello.json"}' \
response.json
                             /\
                             ||
      Substitua test-<your>-<name> pelo nome do seu bucket

Por fim, rode no terminal o seguinte comando:

cat response.json

S3_funcionando

  Se você receber como retorno Yeah, I am working from Insper, Avelinux :), parabéns, você concluiu sua aplicação e ela está funcionando!

Dica Visual

Se entrarmos no dashboard do CloudWatch novamente, conseguiremos ver os logs de acesso registrados para nossas solicitações.

Agora que você já viu todo o ambiente da sua IaaC (Infrastructure as a Code) sendo criado e funcionando, chegou a hora de destruí-lo!

Utilize o comando a seguir no seu terminal para destruir a infraestrutura:

terraform destroy

O resultado final deve ser algo parecido com a imagem a seguir:

resultado

Dica visual

Entre no dashboard da AWS e veja que todos os recursos sumiram: eles foram destruídos!

Conclusão

Parabéns, você acaba de construir (e destruir também) toda uma infraestrutura serverless na AWS! E aí, está se sentindo mais preparado para dar início aos seus próprios projetos?

Você viu como foi bem mais fácil construir toda nossa infraestrutura rodando apenas um conjunto de códigos de uma só vez ao invés de criar serviço por serviço via dashboard da AWS?

A intenção do handout acima era justamente essa! Queria mostrar pra vocês como o uso do Terraform facilita - e muito - o nosso trabalho e também mostrar um pouco o poder do uso da Computação em Nuvem. Lembre-se que o universo Cloud é imenso e possui milhares de possibilidades, então não se restrinja apenas a criar aplicações sem servidor ou sites estáticos! Conheça novos serviços e começe a desfrutar cada vez mais do incrível poder que a Computação em Nuvem pode te oferecer! (E olha que eu nem estou sendo patrocinado para falar tudo isso aqui! Alô @AWS, cadê meu cachê??!!)

Antes de finalizar, confira a seção "Vídeos de aprofundamento" (caso você ainda não tenha conferido) para entender um pouco melhor (e de forma mais visual) o mundo da Computação em Nuvem. Também recomendo conferir a aba "Referências", lá eu deixei vários links que me ajudaram a compreender muito mais a fundo tudo que eu estava desenvolvendo na AWS (e tudo que vocês viram neste handout) durante as últimas 2 semanas (sim, até 2 semanas atrás eu não entendia absolutamente nada de AWS, então se eu consegui aprender tudo isso até aqui, você também consegue).

Por hoje é só! Espero que tenham gostado de todo o material que eu montei aqui. Até a próxima! Tchau :)