Executando verificação de segurança...
13

Construindo uma AWS: S3

Olá a todos, hoje venho mostrar a todos sobre meu novo projeto, estou tentando recriar a AWS do zero (exatamente), a infra por trás da AWS, orquestradores, clusters, HA e por aí vai, e inclusive serviços como EC2, S3, VPC e por aí.

Já publiquei um artigo sobre no TabNews caso alguém tenha interesse sobre.

Bom não vou estender muito, vamos ao que interessa, a criação de um S3, bom como comecei agora o projeto, o S3 que estou fazendo ainda é um simples upload e download de arquivos nada mais, sem restrição(porque ainda não fiz o equivalente a IAM), mas já é algo muito grandioso para mim porque estou estudando Rust, então o servidor web foi inteiro escrito nele, e com esse artigo vou tentar passar minha experiência com ele(que já vou adiantar que foi animal).

Quando você é programador JavaScript (ou qualquer linguagem high level) e vai para um Rust da vida, não vou mentir, assusta um pouco por causa da sintaxe, porém, como um bom brasileiro que sou, não desisto nunca, então acabei desenvolvendo esse webserver simples.

Para desenvolver isso, usei o ActixRS que é um framework web (equivalente ao express só que com mais ecossistema próprio), é uma lib. Rust bem simples, mas super poderosa, não tem muito o que falar dela, caso queira saber mais, só visitar ActixRS que é a documentação oficial deles, lá tem bastante exemplo, vai ajudar muito.

Bom, vamos lá, o S3 comparado aos outros recursos, ele é um dos mais simples. Aqui não vou me estender sobre a parte de ACLs nem nada disso, até porque, ainda, nem banco de dados tem para armazenar isso. Vou mostrar o código que fiz e o principal, a organização das pastas (e isso tem que levar MUITO em conta quando se fala de Rust).

Rust tem uma organização de pastas baseadas em mod, (módulos) onde cada arquivo é um módulo à parte, exemplo.

Dentro da pasta Soure, tem dois arquivos, o main.rs e count.rs. No count tem uma função chamada contar. Para que eu possa usar essa função dentro da main.rs(o entrypoint da aplicação), eu preciso declarar no escopo do arquivo main.rs.

mod count;
use count::contar;

fn main() {
    contar();
}

Então você usa o “mod” count; para especificar ao compilador Rust que você quer importar o módulo count. Logo abaixo tem o use count::contar que é equivalente ao import { contar } from './count'; no TS/JS, por exemplo.

Mas por que estou dizendo isso? Bom, como disse para quem vem do TS/JS e está acostumado a usar o import export isso é MUITO desafiador, vou dar o exemplo do meu projeto e como está a estrutura de pasta.

texto

Bom, como podem ver, são muitas pastas e arquivos, agora como pensei para organizar isso. A resposta é simples: NestJS, sim, eu me baseei no framework NestJS que utiliza uma estrutura onde cada pasta é um serviço do aplicativo e dentro tem os arquivos necessario para funcionar aquele serviço como: upload.module.ts, upload.controller.ts, upload.service.ts.

Mas na minha estrutura tem dois arquivos a mais em uploads/, sendo eles o router.rs e o mod.rs, o que são cada um?

Bom, é de se imaginar que o router faça toda a parte de roteamento do controller, define rotas, os métodos de cada rota e tudo mais. Porém, tem um arquivo chamado mod.rs, que é um arquivo equivalente ao index{.ts, js}, por exemplo, que é um arquivo padrão chamado como exportador de módulos, e o que isso quer dizer? Como expliquei, você tem que definir para o compilador Rust quais são os módulos externos, e expliquei que você pode usar o {nome do mod}.rs, porém tem outro jeito: você colocar o mod.rs em uma pasta que essa pasta será o nome do módulo, no caso upload. Essa foi a melhor forma que encontrei para deixar o código organizado e limpo, onde fica 100% definido de quem é quem.

Agora vamos falar sobre como iniciar um projeto com ActixRS, e é bem simples.

mod upload;

use actix_web::{web, App, HttpServer, Responder};

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new()
            .configure(upload::router::UploadRouter::new)
    })
    .bind("127.0.0.1:8080")?
    .run()
    .await
}

Aqui tem o código do meu main.rs, onde inicio a função main com uma macro do Actix onde espero um retorno de Result (que é basicamente um retorno que espera 2 parâmetros, um de sucesso e outro de erro, mas no nosso caso estamos só passando um parâmetro vazio), depois criamos um servidor web e iniciamos um App novo, fazendo novamente um paralelo com o express, seria algo mais ou menos assim.

import express from 'express';
import http from 'http';

const app = express();
const server = http.createServer(app); 

server.listen(8080);

Então, o seu servidor web já está pronto, porém tem .configure ali que está depois que crio uma nova instância de app, o que isso quer dizer? Bom, vamos vooooltar para JS de novo.

Esse .configure é o equivalente um .use, só que um pouco menos genérico. Com ele, você vai configurar um serviço para fazer configurações específicas para sua aplicação web (vou mostrar mais para frente).

No .configure tem um parâmetro que é a execução do uma nova instância de rota para upload, vamos ver como está no router.rs.

use crate::upload::controller::UploadController;
use actix_web::web;

pub struct UploadRouter;
impl UploadRouter {
    pub fn new(cfg: &mut web::ServiceConfig) {
        cfg.service(
            web::scope("/upload")
                .route("/{bucket}", web::post().to(UploadController::create))
                .route(
                    "/{bucket}/{file:.*}",
                    web::get().to(UploadController::index),
                ),
        );
    }
}

No router.rs está assim, onde criamos uma struct chamada UploadRouter(não vou estender sobre struct, impl e trait, quem quiser saber mais é só acessar aqui).

Então definimos as rotas e os métodos que vão ser executados com forme for causando o match, vou mostrar o que quer dizer isso no JS.

import { Router } from 'express';
import { UserController } from './user.controller';

export class UploadRouter {
  private router: Router;
  private controller: UserController;

  constructor() {
    this.router = Router();
    this.controller = new UserController();
  }

  bootstrap() {
    this.router.get("/upload/:bucket", this.controller.index as RequestHandler);
    this.router.post("/upload/:bucket/:file", this.controller.create as RequestHandler);
    return this.router;
  }
}

Pronto, basicamente é isso aí, criamos um router que faz todo o controle das requisições. Sem muito segredo, agora no UserController ficou assim.

use crate::upload::dto::Upload;
use crate::upload::service::UploadService;

use actix_multipart::form::MultipartForm;
use actix_web::{web, Responder};

use actix_files as fs;

pub struct UploadController;

impl UploadController {
    pub async fn index(
        params: web::Path<(String, String)>,
    ) -> Result<fs::NamedFile, actix_web::Error> {
        UploadService::download(params.0.to_string(), params.1.to_string()).await
    }
    pub async fn create(
        MultipartForm(form): MultipartForm<Upload>,
        bucket: web::Path<String>,
    ) -> Result<impl Responder, actix_web::Error> {
        Ok(UploadService::upload(bucket, actix_multipart::form::MultipartForm(form)).await)
    }
}

Aqui já o negócio começa a ficar doido, criamos uma struct e passamos duas funções para ela, uma chamada index e outra create. A index recebe um parâmetro chamado params do tipo web::Path<(String, String)>, que é o path params que vem da URL e assim repassamos para o UploadService. O path vem em formato de tupla, porque são dois parâmetros, então é assim que pegamos ambos os parâmetros.

Já para o create utilizamos dois parâmetros na função, um é um parâmetro para pegar o path e o outro para pegar o MultipartForm para podermos enviar as imagens para o servidor e salvar em pasta.

O Actix para enviar imagem, também depende de uma lib externa também (porém é própria deles), chamado actix_files que é o equivalente ao multer. Agora vamos ver como a imagem é salva no servidor.

use crate::upload::dto::{Upload, UploadResponse};

use actix_files as fs;
use actix_multipart::form::MultipartForm;
use actix_web::{
    http::{
        header::{ContentDisposition, DispositionType},
        Error,
    },
    web, HttpResponse, Responder,
};

pub struct UploadService;
impl UploadService {
    pub async fn download(
        bucket: String,
        file: String,
    ) -> Result<fs::NamedFile, actix_web::error::Error> {
        let path = format!(
            "{}/temp/{}/{}",
            std::env::current_dir().unwrap().display(),
            bucket,
            file
        );
        if !std::path::Path::new(&path).exists() {
            return Err(actix_web::error::ErrorNotFound("File not found"));
        }
        let file = fs::NamedFile::open(path).unwrap();
        Ok(file
            .use_last_modified(true)
            .set_content_disposition(ContentDisposition {
                disposition: DispositionType::Attachment,
                parameters: vec![],
            }))
    }

    pub async fn upload(
        bucket: web::Path<String>,
        MultipartForm(form): MultipartForm<Upload>,
    ) -> Result<impl Responder, Error> {
        let path = format!(
            "{}/temp/{}",
            std::env::current_dir().unwrap().display(),
            bucket
        );
        std::fs::create_dir_all(path).unwrap();
        for f in form.files {
            let path = format!(
                "{}/temp/{}/{}",
                std::env::current_dir().unwrap().display(),
                bucket,
                f.file_name.unwrap()
            );
            f.file.persist(path.clone()).unwrap();
        }
        Ok(HttpResponse::Ok().json(UploadResponse { success: true }))
    }
}

Na struct UploadService, passamos duas funções, a de upload e a de download. A de upload é tranquila, recebemos dois parâmetros, um é o bucket que será salvo e outro o arquivo em si. O código define o path utilizando a macro format para formatar o texto e sobrescrever em tempo de compilação os {} por dados que desejamos que seja passado como argumento da função em ordem.

Logo em seguida, criamos a pasta do bucket e posteriormente pegamos todos os arquivos enviados ao servidor e os salvamos lá na path. E está pronto o sorvetinho, já conseguimos salvar as imagens no servidor :).

Agora, para fazer o download dela é mais fácil ainda, na função recebemos dois parâmetros, onde um é o bucket e o outro o nome do arquivo em si, definimos novamente o path com o format, depois verificamos se o arquivo e o diretório existem, se não retornamos um erro, se existir, baixaremos.

Pronto, esse é meu super mega blaster mini S3, obviamente vou aprimorar ainda mais ele com ACLs, banco de dados (estou em dúvidas entre o Redis ou etcd).

Esse meu projeto está servindo para estudar toda a infra da AWS e construindo a minha própria, com serviço de orquestrador de contêineres próprio, HA próprio e tudo mais, e ainda por cima construindo os principais serviços da AWS que poderá subir via GUI, obrigado a todos que leu isso e até um futuro próximo 👋.

Carregando publicação patrocinada...
13

Você Tem Curisidade e Coragem. Mas Não Basta.

Parabéns, jovem padawan da nuvem! Reconstruir a AWS é como tentar escalar o Everest. Mas você esta de chinelos.

Seu “S3” atual? Um hello world glorificado. Um NGINX com autoindex on; e um formulário HTML de upload é mais robusto, escalável e seguro que seu código. Mas reconheço sua ambição. Você está no caminho certo… se aguentar a porrada que vem a seguir. Mas no final:

  1. Seu "S3" terá mais fundamento que 99% dos projetos no GitHub.
  2. Você vai rir de termos como "serverless" e "cloud-native".
  3. Recrutadores da AWS vão te mandar DM no LinkedIn.

Aula 1: C ou Morte – Onde a Infra Nasce (e Seus Sonhos Morrem?)

Verdade Cruel: Actix é um playground para crianças. Enquanto você brinca de route("/upload"), o kernel śo entende epoll.h sockets.h, pthreads.h para lidar com milhões de conexões. Seu código Rust é um disfarce bonito para a ignorância.

Lição: C te ensina o que Rust esconde: alocação de memória, syscalls, e o gosto amargo do fracasso.

Exercícios de Humilhação:

  1. Desafio Fácil: Escreva um servidor HTTP em C que responde 'Hello World'. Use socket(), bind(), listen(), accept().
    Vai travar na primeira conexão. Bem-vindo à vida real.

  2. Desafio Médio: Adicione suporte centenas de concorrentes com pthreads.h.

  3. Desafio Dificl: Suporte a milhares conexões concorrentes com um thread pool

  4. Desafio Hardcore: Escalone para dezenas de milhares de conexoes usando epoll e threads

Aula 2: Filesystems – Onde Seus Dados Choram (e Você Também)

Verdade Cruel: Seu “S3” salva arquivos em pastas como um estagiário bêbado. Você está usando uma vassoura para cavar um buraco.

Lição: Dados são sagrados. Se você não trata eles como tal, seu S3 é um sacrilégio.

Exercícios de Sadismo Criativo :

  1. Desafio Fácil: Configure ZFS numa VM. Crie um snapshot, corrompa um arquivo, e restaure.
  2. Desafio Médio: Crie um filesystem FUSE em Rust que representa arquivos e pastas como blobs e metadata no SQLite.
  3. Desafio Hardcore: Implemente Merkle Trees para detectar corrupção de arquivos. Divida um arquivo em chunks, gere hashes, e reconstrua como um quebra-cabeça.

Aula 3: Redes – Onde Pacotes Viram Pesadelos (e Você Vira Engenheiro de Infra)

Verdade Cruel: Seu S3 não sabe o que é uma ameaça. Enquanto isso, a AWS usa eBPF/XDP para filtrar ataques no kernel.

Lição: Rede é guerra. Se você não domina o kernel, é só um soldado de brinquedo.

Exercícios de Tortura Kerneliana:

  1. Desafio Fácil: Bloqueie IPs com mais de 100 reqs/segundo usando iptables.
  2. Desafio Hardcore: Use eBPF para medir o tempo entre accept() e write() no disco.
  3. Desafio Insano: Redirecione tráfego entre VMs com XDP.

Aula Final: Formatura do Jardim de Infância da Infra.

Você é um Diamante. Enquanto outros brincam de Kubernetes, você está cavando as camadas mais profundas.

A AWS começou com um servidor PHP e um sonho. Você começou com Rust e arrogância. Mas daqui a 10 anos, talvez eu trabalhe para você.

Projeto Final: Reescreva seu S3 com

  • hyper.rs
  • Rate limiting via eBPF
  • Sistema de arquivos em ZFS

Depois, volte aqui. Vou te ensinar como construir um hipervisor do zero.

Um abraço e bons estudos!

5

Muto obrigado pelas "pancadas", vou estudar mais sobre, vou fazer tipo uma série aqui no TabNews mostrando a evolução desse projeto, espero que me ajude com isso 👍.
Então quero estudar mais sobre C, porém ainda não me sinto seguro(é doideira isso mas sim), estou criando esse projeto para me OBRIGAR a entender de programação low-level, acho que Rust é uma porta de entrada MUIIITO melhor do que C/C++ pra isso, já que a sintaxe dele lembra bastante do JS por exemplo, diferente do C/C++, então quero ir devagar nisso, estou querendo usar coisas "prontas" tipo LXC, KVM, ZFS(como você mencionou para criar volumes de dados e distribuir isso em servidores diferentes), estou usando OVS(Open Virtual Switch) para criar adaptadores de rede virtual para usar VXLAN para conectar servidores em L2 passando por L3, acho que vai ficar muito bom, novamente muito obrigado por mostrar sobre estudar

2

Rust parece JS? Ilusão. JS foi inspirado em C. Rust é C++ disfarçado de moderno.

  1. C vs C++ vs Rust:

C (K&R): 150 páginas. Simples e Direto.

C++ (Stroustrup): 2000 páginas. Monstro cheio de bagagem legada.

Rust (Livro Oficial): 600 páginas. Nem cobre unsafe direito.

  1. Compilar para nativo sem GC ≠ Baixo nível:

Baixo nível: Escrever drivers, manipular page tables, criar um scheduler. Lidar com os bits na CPU e barramentos.

  1. C é o Passado e o Presente (Rust pode ser Futuro):
    Linux, ZFS, KVM, e 99% das infraestruturas globais são C. Ignorar isso é como querer ser chef sem saber fritar um ovo.

Verdade Final:

Rust é útil? Sim. Melhor que C para iniciar no low-level? Nunca. C é a base. Sem ela, você é um turista da computação.

P.S.: Quando seu código C der segfault pela 100ª vez, você vai entender por que Rust existe. Mas primeiro, entenda o problema.