Implementando AWS Well-Architected Framework com Pulumi

Coffee DevOps
ZRP Techblog
9 min readMar 20, 2023

--

Arte desenvolvida por Gregorio Cavallari

A implementação de uma infraestrutura robusta e segura é fundamental para o sucesso de qualquer aplicação em nuvem. Para ajudar os usuários a criarem infraestruturas de alta qualidade, a Amazon Web Services (AWS) criou o Well-Architected Framework. Este framework fornece diretrizes e define melhores práticas para ajudar a projetar, avaliar e melhorar sua infraestrutura na nuvem da AWS.

Mas a criação de uma infraestrutura seguindo os princípios do Well-Architected Framework pode ser uma tarefa complexa e demorada, especialmente quando feita manualmente. É aí que entra o Pulumi, uma plataforma de gerenciamento de infraestrutura como código.

# tldr

https://github.com/gabrielsclimaco/pulumi-aws-waf-article

O Pulumi permite que a infraestrutura seja criada, gerenciada e automatizada na nuvem da AWS (entre outras plataformas de Cloud), usando linguagens de programação que você provavelmente já tem familiaridade, como TypeScript, Python e Go.

Neste artigo vamos mostrar como implementar o Well-Architected Framework da AWS utilizando o Pulumi com Typescript. Discutiremos as vantagens de usar o Pulumi, como criar uma infraestrutura seguindo os princípios propostos pelo framework, junto a um exemplo prático de implementação.

O que é o AWS Well-Architected Framework

O AWS Well-Architected Framework é um conjunto de práticas recomendadas, desenvolvidas pela Amazon Web Services (AWS), para ajudar empresas e organizações a projetar e operar aplicativos e infraestrutura na nuvem.

O framework é baseado em cinco pilares fundamentais:

  • Excelência operacional (Operational Excellence): busca otimizar continuamente as operações e processos, melhorando a eficiência, a resiliência e a capacidade de rec- uperação em caso de falhas.
  • Segurança (Security): prioriza a proteção dos dados, identidade e recursos, além de promover a conformidade com as políticas e regulamentações de segurança.
  • Confiabilidade (Reliability): visa garantir a disponibilidade, a escalabilidade e a tolerância a falhas das aplicações, minimizando os impactos de possíveis interrupções.
  • Eficiência de desempenho (Performance Efficiency): busca maximizar o uso dos recursos computacionais, reduzindo os custos e melhorando o desempenho das aplicações.
  • Otimização de custos (Cost Optimization): busca reduzir os custos operacionais, eliminando desperdícios e otimizando a utilização dos recursos disponíveis na nuvem.

Cada pilar inclui práticas recomendadas e diretrizes que ajudam os usuários a identificar possíveis problemas e oportunidades de melhoria em suas arquiteturas e processos.

O framework é aplicável a uma ampla gama de casos de uso e setores, desde aplicativos web simples até soluções de grande escala, como processamento de dados, big data e aprendizado de máquina. Ele é projetado para ajudar as empresas a atender a requisitos de conformidade, aumentar a segurança e a confiabilidade de suas infraestruturas e reduzir os custos operacionais.

O que é o Pulumi e por que usá-lo

O Pulumi é uma plataforma de gerenciamento de infraestrutura como código que permite que desenvolvedores escrevam código para criar e gerenciar recursos na nuvem, como redes privadas, máquinas virtuais, bancos de dados, entre outros, usando uma, ou mais, linguagens de programação, dentre TypeScript, JavaScript, Python, Go, .NET, Java e YAML (que não é uma linguagem de progamação, mas todo mundo gosta de YAML ❤️).

Além disso, o Pulumi oferece uma série de recursos avançados, como automação de pipelines de delivery, suporte à testes unitários, de aceitação e de integração, criptografia de valores sensíveis, integrações via API, entre outras funcionalidades. Além disso também possui compatibilidade com Terraform, uma das ferramentas mais populares de infraestrutura como código.

O Pulumi simplifica e agiliza a implementação de infraestuturas na nuvem. Ao invés de criar recursos manualmente através da interface da AWS você pode automatizar o processo inteiro, garantindo assim maior precisão, consistência e facilidade na hora de implementar sua infraestrutura.

Como implementar o Well-Architected Framework da AWS com o Pulumi

Pré requisitos

Antes de começar é necessário:

Iniciando um projeto

Este exemplo irá criar uma aplicação web hospedada na AWS usando um balanceador de carga, um servidor de aplicação e um banco de dados MySQL.

Para começar, crie uma nova pasta para o projeto e em seguida utilize a CLI do Pulumi para criar um projeto:

mkdir pulumi-new-project && cd pulumi-new-project
pulumi new aws-typescript

Siga o passo a passo dando o nome que desejar para a aplicação. Utilize também a região da AWS de sua preferência, neste exemplo vou usar a us-west-1:

This command will walk you through creating a new Pulumi project.

Enter a value or leave blank to accept the (default), and press <ENTER>.
Press ^C at any time to quit.

project name: (pulumi-aws-article) pulumi-aws-waf
project description: (A minimal AWS TypeScript Pulumi program)
Created project 'pulumi-aws-waf'

Please enter your desired stack name.
To create a stack in an organization, use the format <org-name>/<stack-name> (e.g. `acmecorp/dev`).
stack name: (dev)
Created stack 'dev'

aws:region: The AWS region to deploy into: (us-east-1) us-west-1
Saved config

Installing dependencies...


added 169 packages, and audited 170 packages in 2m

54 packages are looking for funding
run `npm fund` for details

found 0 vulnerabilities
Finished installing dependencies

Your new project is ready to go!

To perform an initial deployment, run `pulumi up`

Criando recursos com Pulumi

Agora abra o arquivo index.ts no seu editor favorito e vamos começar criando uma VPC e um internet gateway para os recursos ficarem acessíveis pela internet:

import * as pulumi from '@pulumi/pulumi';
import * as aws from '@pulumi/aws';

// Criação do recurso de VPC
const vpc = new aws.ec2.Vpc('vpc', {
cidrBlock: '10.0.0.0/16',
});

// Criando um internet gateway para a VPC
const igw = new aws.ec2.InternetGateway('my-igw', {
vpcId: vpc.id,
});

Em seguida vamos criar duas subnets públicas para a aplicação, junto de uma tabela de roteamento para cada subnet poder ser acessada pelo internet gateway:

// Criando uma subnet pública para a aplicação
const webSubnet1 = new aws.ec2.Subnet('web-subnet-1', {
cidrBlock: '10.0.1.0/24',
vpcId: vpc.id,
mapPublicIpOnLaunch: true,
availabilityZone: aws.getAvailabilityZones().then((zones) => zones.names[0]),
});

// Criando uma subnet pública para a aplicação
const webSubnet2 = new aws.ec2.Subnet('web-subnet-2', {
cidrBlock: '10.0.2.0/24',
vpcId: vpc.id,
mapPublicIpOnLaunch: true,
availabilityZone: aws.getAvailabilityZones().then((zones) => zones.names[1]),
});

// Criando uma tabela de roteamento para as subnets publicas ficarem acessíveis pelo internet gateway
const publicRouteTable = new aws.ec2.RouteTable('public-route-table', {
vpcId: vpc.id,
routes: [
{
cidrBlock: '0.0.0.0/0',
gatewayId: igw.id,
},
],
});

// Associando a tabela de roteamneto a primeira vpc publica
const publicRouteAssociation1 = new aws.ec2.RouteTableAssociation(
'public-route-association-1',
{
routeTableId: publicRouteTable.id,
subnetId: webSubnet1.id,
},
);

// Associando a tabela de roteamneto a segunda vpc publica
const publicRouteAssociation2 = new aws.ec2.RouteTableAssociation(
'public-route-association-2',
{
routeTableId: publicRouteTable.id,
subnetId: webSubnet2.id,
},
);

Vamos criar também duas subnets para o banco de dados, porém privadas:

// Criando uma subnet privada para o banco de dados rds
const dbSubnet1 = new aws.ec2.Subnet('db-subnet-1', {
cidrBlock: '10.0.3.0/24',
vpcId: vpc.id,
availabilityZone: aws.getAvailabilityZones().then((zones) => zones.names[0]),
});

// Criando uma subnet pública para o servidor web
const dbSubnet2 = new aws.ec2.Subnet('db-subnet-2', {
cidrBlock: '10.0.4.0/24',
vpcId: vpc.id,
availabilityZone: aws.getAvailabilityZones().then((zones) => zones.names[1]),
});

Para finalizar os recursos de rede, vamos criar um grupo de segurança para cada um dos mesmos recursos. O grupo de segurança do servidor web deve permitir apenas trafego para o acesso da aplicação, enquanto o banco de dados deve ser acessivel somente pelo servidor da aplicação:

// Criando um grupo de segurança para permitir o acesso HTTP
const webSg = new aws.ec2.SecurityGroup("web-sg", {
vpcId: vpc.id,
ingress: [
{ protocol: "tcp", fromPort: 80, toPort: 80, cidrBlocks: ["0.0.0.0/0"] },
],
});

// Criando um grupo de segurança para permitir o acesso aos dados
const dbSg = new aws.ec2.SecurityGroup("db-sg", {
vpcId: vpc.id,
ingress: [
{ protocol: "tcp", fromPort: 3306, toPort: 3306, cidrBlocks: ["10.0.0.0/16"] },
],
});

Para garantir maior segurança dos dados vamos setar o parâmetro storageEncrypted=true para garantir que os dados sejam criptografados enquanto não são lidos.

Além disso vamos setar também como true o parâmetro multiAz para garantir que o banco seja criado em mais de uma zona de disponibilidade com failover automático, garantindo assim uma maior disponibilidade dos dados.

Utilizando a respectiva subnet e grupo de segurança a implementação fica:

const dbSubnetGroupName = 'my-db-group';

// Criando um grupo de subnets para associar ao banco
const dbSubnetGroup = new aws.rds.SubnetGroup('my-db-group', {
name: dbSubnetGroupName,
subnetIds: [dbSubnet1.id, dbSubnet2.id],
});

// Criando um banco de dados RDS
const db = new aws.rds.Instance(
'my-db',
{
engine: 'mysql',
instanceClass: 'db.t2.small',
allocatedStorage: 10,
dbSubnetGroupName,
vpcSecurityGroupIds: [dbSg.id],
username: 'admin',
password: 'password',
storageEncrypted: true,
multiAz: true,
},
{
dependsOn: dbSubnetGroup,
},
);

Agora vamos criar o servidor da aplicação web a respectiva subnet e grupo de segurança:

// Criando um'a instância EC2 para o servidor web
const instance = new aws.ec2.Instance('my-instance', {
instanceType: 't2.micro',
ami: 'ami-00569e54da628d17c',
vpcSecurityGroupIds: [webSg.id],
subnetId: webSubnet1.id,
userData: pulumi.interpolate`#!/bin/bash
echo "Pulumi + AWS = <3" > index.html
nohup python -m SimpleHTTPServer 80 &`,
});

Vamos criar também um balanceador de carga para a intância para distribuir o trafego e garantir maior estabilidade:

// Criando um balanceador de carga
const lb = new aws.lb.LoadBalancer('my-lb', {
internal: false,
subnets: [webSubnet1.id, webSubnet2.id],
});

// Criando um target group para o baleaceador de carga
const targetGroup = new aws.lb.TargetGroup(
'web-target',
{
port: 80,
protocol: 'HTTP',
vpcId: vpc.id,
targetType: 'instance',
healthCheck: {
protocol: 'HTTP',
port: '80',
path: '/',
timeout: 10,
interval: 30,
matcher: '200-299',
},
},
{
dependsOn: instance,
},
);

// Criando um listener para o baleaceador de carga
const listener = new aws.lb.Listener('web-listener', {
loadBalancerArn: lb.arn,
port: 80,
protocol: 'HTTP',
defaultActions: [
{
type: 'forward',
targetGroupArn: targetGroup.arn,
},
],
});

// Anexando a instância ao target group para direcionar o trafego
const instanceTarget = new aws.lb.TargetGroupAttachment(
'web-target-attachment',
{
targetGroupArn: targetGroup.arn,
targetId: instance.id,
port: 80,
},
);

Por fim vamos expor algumas informações sobre os recursos para que possamos acessá-los:

// Exportando informações sobre a aplicação
export const lbDnsName = lb.dnsName;
export const dbEndpoint = db.endpoint;

Criando os recursos na AWS

Uma vez que tudo está de acordo com o desejado no código podemos utilizar a CLI do Pulumi para que ele possa de fato criar os recursos na AWS:

pulumi up

Previewing update (dev)

View Live: https://app.pulumi.com/<username>/pulumi-aws-waf/dev/previews/<uuid>

Type Name Plan
+ pulumi:pulumi:Stack pulumi-aws-waf-dev create
+ ├─ aws:ec2:Vpc vpc create
+ ├─ aws:ec2:InternetGateway my-igw create
+ ├─ aws:ec2:SecurityGroup db-sg create
+ ├─ aws:ec2:SecurityGroup web-sg create
+ ├─ aws:ec2:RouteTable public-route-table create
+ ├─ aws:ec2:Subnet db-subnet-2 create
+ ├─ aws:ec2:Subnet web-subnet-2 create
+ ├─ aws:ec2:Subnet web-subnet-1 create
+ ├─ aws:ec2:Subnet db-subnet-1 create
+ ├─ aws:ec2:RouteTableAssociation public-route-association-2 create
+ ├─ aws:ec2:Instance my-instance create
+ ├─ aws:ec2:RouteTableAssociation public-route-association-1 create
+ ├─ aws:lb:LoadBalancer my-lb create
+ ├─ aws:rds:SubnetGroup my-db-group create
+ ├─ aws:rds:Instance my-db create
+ ├─ aws:lb:TargetGroup web-target create
+ ├─ aws:lb:TargetGroupAttachment web-target-attachment create
+ └─ aws:lb:Listener web-listener create


Outputs:
dbEndpoint: output<string>
lbDnsName : output<string>

Resources:
+ 19 to create

Do you want to perform this update? yes
Updating (dev)

View Live: https://app.pulumi.com/<username>/pulumi-aws-waf/dev/updates/1

Type Name Status
+ pulumi:pulumi:Stack pulumi-aws-waf-dev created (620s)
+ ├─ aws:ec2:Vpc vpc created (3s)
+ ├─ aws:ec2:InternetGateway my-igw created (1s)
+ ├─ aws:ec2:Subnet db-subnet-1 created (2s)
+ ├─ aws:ec2:Subnet web-subnet-1 created (13s)
+ ├─ aws:ec2:Subnet db-subnet-2 created (2s)
+ ├─ aws:ec2:SecurityGroup db-sg created (5s)
+ ├─ aws:ec2:Subnet web-subnet-2 created (13s)
+ ├─ aws:ec2:SecurityGroup web-sg created (5s)
+ ├─ aws:ec2:RouteTable public-route-table created (3s)
+ ├─ aws:rds:SubnetGroup my-db-group created (3s)
+ ├─ aws:rds:Instance my-db created (602s)
+ ├─ aws:ec2:RouteTableAssociation public-route-association-1 created (1s)
+ ├─ aws:ec2:Instance my-instance created (24s)
+ ├─ aws:ec2:RouteTableAssociation public-route-association-2 created (1s)
+ ├─ aws:lb:LoadBalancer my-lb created (125s)
+ ├─ aws:lb:TargetGroup web-target created (2s)
+ ├─ aws:lb:TargetGroupAttachment web-target-attachment created (0.61s)
+ └─ aws:lb:Listener web-listener created (1s)


Outputs:
dbEndpoint: "my-xxxxxxxxx.xxxxxxxxxxxx.us-west-1.rds.amazonaws.com:3306"
lbDnsName : "my-lb-xxxxxxx-xxxxxxxxxx.us-west-1.elb.amazonaws.com"

Resources:
+ 19 created

Duration: 10m22s

Após criar os recursos o Pulumi salva o estado da infraestrutura na plataforma para servir de base de comparação em futuras implementações e garantir que o que está na AWS reflete o que está no código.

Podemos utilizar o DNS do balanceador de carga exposto pelo output lbDnsName para acessar a aplicação:

Navegador acessando o DNS do load balancer

Destruindo os recursos na AWS

Lembre-se de destruir os recursos caso não vá mais utilizá-los e evite uma cobrança desnecessária no cartão de crédito:

pulumi destroy

Conclusão

Por meio do AWS Well-Architected Framework as empresas podem projetar e gerenciar aplicativos e infraestrutura em nuvem de maneira mais eficiente e econômica, reduzindo riscos e minimizando custos.

É uma ferramenta valiosa para qualquer empresa que queira aproveitar ao máximo os benefícios da nuvem AWS.

O Pulumi entra como uma ferramenta adicional para auxiliar e simplificar a implementação do framework, reduzindo a possibilidade de erro humano com a familiaridade de uma linguagem de programação que você já conhece e utiliza, além de garantir a implementação das melhores práticas de Delivery e DevOps.

Caso queria ver o código completo ele está dispoível no repositório https://github.com/gabrielsclimaco/pulumi-aws-waf-article.

Obrigado por ler até aqui e todo feedback nos comentários será muito bem vindo.

#paz ☕️

--

--

Coffee DevOps
ZRP Techblog

25 y.o. He/Him. A software developer with a passion for infrastructure. Currently working at ZRP | iFut. You can call me Coffee