Hands-on: como ampliar eficiência em infraestrutura em nuvem com soluções em AWS
por Victor Luiz Domingues, Engenheiro de Software no Íon Itaú
A eficiência é um dos pilares da cultura do Itaú Unibanco e um passo importante para uma estratégia voltada para a centralidade no cliente. Com processos mais eficientes, termos mais tempo e recursos para nos dedicarmos em melhorias contínuas para atendermos as necessidades de clientes de forma mais rápida e efetiva.
Com base neste entendimento, vamos abordar neste artigo como você pode ampliar a eficiência da sua equipe em um cenário de atuação em AWS, com a aplicação de recursos como AWS Lambda, AWS Systems Manager — Parameter Store, AWS Secrets Manager e e Stack .NET, além de propor a implementação de uma solução em .NET 6, utilizando Terraform para automação da infraestrutura da função Lambda. Por fim, também apresentaremos uma alternativa para reduzir o consumo de recursos e requisições feitas para a AWS (Secrets e Systems Manager), e entenderemos quais os benefícios da solução proposta.
Contexto
Em meados de novembro de 2022, foi publicado no blog oficial da AWS um artigo sobre que fornece algumas informações sobre o caso de uso que exploraremos em detalhes neste texto. Em resumo, o artigo em questão sugere a implementação de cache em memória utilizando um lambda layer (AWS Parameter Store and Secrets Manager Lambda Extension) que expõe um servidor HTTP capaz de abstrair a implementação de cache às chamadas para os recursos AWS que o SDK normalmente faria. Essa proposta sugere a diminuição no uso de recursos, redução nas chamadas para os recursos AWS e propõe uma possível redução de latência, uma vez que o acesso dos para recursos estão cacheados na memória desse layer.
Caso de uso
Imagine que você tem uma função Lambda na AWS, implementada em .NET, cuja mesma aplicação é executada após o recebimento de um Evento SQS e deve realizar uma consulta na WEB API de um parceiro. As informações referentes à comunicação HTTP, como servidor (HOST) e rota para a chamada dessa API, estão armazenadas na Parameter Store, e a chave de acesso gerado pelo seu parceiro está armazenada de forma segura no Secrets Manager. Podemos facilmente recuperar os parâmetros e segredos através do SDK da AWS (AWSSDK.SimpleSystemsManagement, AWSSDK.SecretsManager). Porém, o SDK realiza requisições para as APIS de recursos da AWS conforme o número de instancias da função lambda aumenta, e no pior dos casos, à medida em que o Handler da função lambda é executado.
Ou seja, como mencionado, o lambda layer irá expor um servidor local que armazenará os valores de parâmetros e segredo em cache, conforme o servidor é exposto no host local na porta 2773 (http:localhost: 2773)
Para preenchermos o header X-Aws-Parameters-Secrets-Token, será necessário recuperar o valor da variável de ambiente AWS_SESSION_TOKEN fornecida em tempo de execução dentro do ambiente da AWS. Para testes, você pode mockar esse valor: caso você tenha a AWS CLI configurada corretamente em sua máquina, após fazer login, esse valor será definido automaticamente. Aqui, tenha atenção no valor da variável de ambiente PARAMETERS_SECRETS_EXTENSION_CACHE_ENABLED, que é o que habilita o cache.
Para referência, você pode conferir outras configurações e variáveis de ambiente na documentação oficial da AWS.
Hands-on: setups, contratos e como recuperar segredos e parâmetros
A seguir, demonstraremos o passo a passo de como implementar a solução que habilita o lambda layer.
launchSettings.json #
Em sua IDE ou editor de texto, configure o arquivo launchSettings.json para rodar localmente a extensão:
{
"profiles": {
"Mock Lambda Test Tool": {
"commandName": "Executable",
"commandLineArgs": "--port 5050",
"workingDirectory": ".\\bin\\$(Configuration)\\net6.0",
"executablePath": "%USERPROFILE%\\.dotnet\\tools\\dotnet-lambda-test-tool-6.0.exe",
"environmentVariables": {
"PARAMETERS_SECRETS_EXTENSION_HTTP_PORT": "2773",
// "AWS_SESSION_TOKEN": "AWS_TOKEN"
// caso não esteja com as credenciais da AWS em sua maquina.
}
}
}
}
Contratos
De forma breve, os contratos representam as interfaces de comunicação dos endpoints da API da extensão. Eles representam os parâmetros e dados obrigatórios de requisição e resposta.
Recuperar Segredos
Para recuperar os segredos, faça uma requisição HTTP via método GET para o endpoint /secretsmanager/get, respeitando o contrato demonstrado a seguir.
GET
/secretsmanager/get
Parâmetros:
* secretId: [ID_DO_SEGREDO]
Headers:
Content-Type: application/json
* X-Aws-Parameters-Secrets-Token: VALOR_AWS_SESSION_TOKEN
Abaixo, veja o exemplo demonstrado através de uma chamada CURL:
curl --location --request GET 'http://localhost:2773/secretsmanager/get?secretId=MEU_SEGREDO' \
--header 'X-Aws-Parameters-Secrets-Token: VALOR_AWS_SESSION_TOKEN'
A seguir, veja qual será a resposta devolvida pela API da extensão:
{
"ARN":"arn:aws:secretsmanager:us-west-2:123456789012㊙️MyTestDatabaseSecret-a1b2c3",
"CreatedDate":1.523477145713E9,
"Name":"MyTestDatabaseSecret",
"SecretString":"{\n \"username\":\"david\",\n \"password\":\"EXAMPLE-PASSWORD\"\n}\n",
"VersionId":"EXAMPLE1-90ab-cdef-fedc-ba987SECRET1"
}
Recuperar Parâmetros
Para recuperar os parâmetros, faça uma requisição HTTP via método GET para o endpoint /systemsmanager/parameters/get, respeitando o contrato demonstrado a seguir.
GET
/systemsmanager/parameters/get
Parâmetros:
* name: [NOME_DO_PARAMETRO]
version: [NÚMERO_VERSAO]
label: [LABEL]
withDecryption: [TRUE_FALSE]
Headers:
Content-Type: application/json
* X-Aws-Parameters-Secrets-Token: VALOR_AWS_SESSION_TOKEN
Veja abaixo o exemplo demonstrado através de uma chamada CURL:
curl --location --request GET http://localhost:2773/systemsmanager/parameters/get?name=MEU_PARAMETRO' \
--header 'X-Aws-Parameters-Secrets-Token: VALOR_AWS_SESSION_TOKEN'
A seguir, confira a resposta devolvida pela API da extensão:
{
"Parameter": {
"ARN": "arn:aws:ssm:us-east-2:111122223333:parameter/MyGitHubPassword",
"DataType": "text",
"LastModifiedDate": 1582657288.8,
"Name": "MyGitHubPassword",
"Type": "SecureString",
"Value": "AYA39c3b3042cd2aEXAMPLE/AKIAIOSFODNN7EXAMPLE/fh983hg9awEXAMPLE==",
"Version": 3
}
}
Implementação em .NET 6, modelos e functions
Neste tópico, demonstraremos a modelagem para a implementação do consumo da API do lambda layer em uma aplicação Lambda em .NET 6. Serão modelados os contratos de requisição e resposta para integração com a extensão via HTTP, seguindo as diretrizes apresentadas nos trechos anteriores.
Modelos
Monte os modelos de resposta da API do lambda layer pelos seguintes records:
public record Parameter(string Name, string Value, int Version);
public record GetParameterResponse(Parameter Parameter);
public record GetSecretValueResponse(string Name, string SecretString);
Function.cs
Nessa classe, as requisições HTTP para API foram feitas utilizando o HttpClient. Também utilizamos o QueryHelpers.AddQueryString para formatar os parâmetros da query string e o método BuildPath, que
devolve o path da URI com a query string formatada.
public class Function
{
public Function() { }
public async Task FunctionHandler(SQSEvent evnt, ILambdaContext context)
{
var port = Environment.GetEnvironmentVariable("PARAMETERS_SECRETS_EXTENSION_HTTP_PORT");
var sessionToken = Environment.GetEnvironmentVariable("AWS_SESSION_TOKEN");
using var httpClientLayer = new HttpClient();
httpClientLayer.BaseAddress = new Uri($"http://localhost:{port}");
httpClientLayer.DefaultRequestHeaders.Add("X-Aws-Parameters-Secrets-Token", sessionToken);
var pathSecret = BuildPath(path: "/secretsmanager/get",
key: "secretId",
value: "/Partner/ServiceToken");
var pathParameter = BuildPath(path: "/systemsmanager/parameters/get",
key: "name",
value: "/Partner/Host");
var serviceTokenSecret = await httpClientLayer.GetFromJsonAsync<GetSecretValueResponse>(pathSecret);
var partnerHostParameter = await httpClientLayer.GetFromJsonAsync<GetParameterResponse>(pathParameter);
}
private static string BuildPath(string path, string key, string value)
{
var query = new Dictionary<string, string>(1)
{
[key] = value
};
return QueryHelpers.AddQueryString(path, query);
}
}
Automação da Infraestrutura em Terraform
Para esse exemplo, vamos criar a automação por meio do Terraform. Não se preocupe: a mesma automação tem equivalência para CloudFormation.
Para começar, configure previamente a infraestrutura da lambda, criando as roles com suas respectivas permissões.
data.tf
No Terraform, vamos usar um “data” para representar a política que devemos criar a seguir, para que a lambda consiga iniciar e consumir o lambda layer.
data "aws_iam_policy_document" "AWSLambdaTrustPolicy" {
statement {
actions = ["sts:AssumeRole"]
effect = "Allow"
principals {
type = "Service"
identifiers = ["lambda.amazonaws.com"]
}
}
}
locals.tf
No Terraform, em locals, definimos algumas informações que vamos usar a seguir para configurar o recurso lambda.
locals {
lambda_name = "lambda_extensions_net6"
runtime = "dotnet6"
filename = "${path.module}/../src/LambdaExtensions/bin/Release/net6.0/LambdaExtensions.zip" # dotnet lambda package
file_exists = fileexists(local.filename)
}
Em seguida, configure a lambda com o layer necessário para que a utilização do cache seja bem-sucedida. As versões da layer estão disponíveis na documentação oficial. Adicione a layer arn:aws:lambda:sa-east-1:933737806257:layer:AWS-Parameters-and-Secrets-Lambda-Extension:4 no recurso aws_lambda_function.
main.tf
Em main, vamos efetivar a criação dos recursos, do role com a policy previamente declarada e da função lambda com a extensão de lambda layer devidamente configurada.
resource "aws_iam_role" "lambda_role" {
name = "lambda_role"
assume_role_policy = data.aws_iam_policy_document.AWSLambdaTrustPolicy.json
}
resource "aws_iam_role_policy_attachment" "terraform_lambda_policy" {
role = aws_iam_role.lambda_role.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
}
resource "aws_lambda_function" "lambda_extensions_net6" {
function_name = local.lambda_name
handler = "LambdaExtensions::LambdaExtensions.Function::FunctionHandler"
runtime = local.runtime
role = aws_iam_role.lambda_role.arn
filename = local.filename
source_code_hash = filebase64sha256(local.filename)
timeout = 30
memory_size = 128
environment {
variables = {
PARAMETERS_SECRETS_EXTENSION_CACHE_ENABLED = "TRUE"
PARAMETERS_SECRETS_EXTENSION_HTTP_PORT = "2773"
# PARAMETERS_SECRETS_EXTENSION_LOG_LEVEL = "DEBUG"
# veja todas as variaveis de ambiente
# https://docs.aws.amazon.com/systems-manager/latest/userguide/ps-integration-lambda-extensions.html
}
}
layers = ["arn:aws:lambda:sa-east-1:933737806257:layer:AWS-Parameters-and-Secrets-Lambda-Extension:4"]
# Escolha a ARN de sua região
# https://docs.aws.amazon.com/systems-manager/latest/userguide/ps-integration-lambda-extensions.html
}
output "lambda_file_exists" {
value = local.file_exists
description = "Filename exists"
}
Conclusão
A solução proposta é bem promissora e fácil de se implementar, por ser baseada em conceitos já bem definidos na computação, como cache em memória e protocolo HTTP. Ela reduz o número de requisições para os recursos AWS (Systems e Secrets Manager) e, apesar de aqui utilizarmos um exemplo em .NET 6, é agnóstica à linguagem ou Stack, uma vez que um lambda layer pode ser vinculada a qualquer função lambda usando o runtime de sua preferência.
É possível enumerarmos quais os principais benefícios de uso dessas ferramentas, que consistem em uma redução de latência e redução de chamadas para AWS System e Secrets Manager, além da redução de custos para AWS System e Secrets Manager.
Chegamos ao fim do artigo. Se você curtiu o conteúdo ou ficou com alguma dúvida, deixe um comentário abaixo!