DynamoDB Local Cresceu: Uma Analise Pratica do ExtendDB

O DynamoDB Local tem sido o substituto de laptop para o AWS DynamoDB desde 2013. E um JAR Java, roda em memoria ou em um arquivo SQLite, aceita quase qualquer formato de requisicao, nao tem autenticacao real e trata Streams de forma bastante flexivel. E bom para testes unitarios. Comeca a falhar no momento em que seu codigo faz algo alem de PutItem e GetItem.
O ExtendDB v0.1.0 acabou de ser lancado. E uma implementacao do protocolo wire do DynamoDB escrita do zero, em Rust, por engenheiros da AWS, com PostgreSQL como backend, Apache 2.0. A proposta e "DynamoDB Local, mas que voce pode levar a serio." Este post e uma analise pratica para verificar se isso se sustenta, executado como um laboratorio lado a lado em um unico Mac.
O que o ExtendDB realmente e
O ExtendDB nao e um fork do DynamoDB. Nao ha codigo fonte do DynamoDB nele. O que ele faz e falar o protocolo wire do DynamoDB (o dispatch JSON-1.0 X-Amz-Target que os SDKs da AWS usam), entao um cliente boto3 ou AWS CLI existente pode ser apontado para um endpoint ExtendDB com uma unica mudanca de flag e funciona, sem necessidade de modificacoes.
Internamente, e um workspace Rust pequeno e focado:
flowchart LR
bin[extenddb<br/>CLI + daemon] --> server[extenddb-server<br/>HTTP + console]
server --> engine[extenddb-engine<br/>manipuladores de op DynamoDB]
server --> auth[extenddb-auth<br/>SigV4 + politica IAM]
engine --> core[extenddb-core<br/>tipos, expressoes]
engine --> storage[extenddb-storage<br/>definicoes de trait]
storage --> pg[extenddb-storage-postgres<br/>backend PostgreSQL]O crate extenddb-storage contem apenas definicoes de trait. extenddb-storage-postgres e a unica implementacao hoje. A interface e limpa o suficiente para que um backend diferente (sqlite, foundationdb, qualquer um) se resumiria principalmente a implementar os traits. Aqui esta o trait de ciclo de vida, do crates/storage/src/lib.rs:
/// Todos os metodos recebem `account_id` para escopo de operacoes em uma unica conta.
/// Isso permite isolamento multi-account: contas diferentes podem ter tabelas
/// com o mesmo nome sem conflito.
pub trait TableEngine: Send + Sync {
fn create_table(
&self,
account_id: &str,
input: CreateTableInput,
) -> BoxFuture<'_, Result<TableDescription, StorageError>>;
fn delete_table(
&self,
account_id: &str,
input: DeleteTableInput,
) -> BoxFuture<'_, Result<TableDescription, StorageError>>;
fn describe_table(
&self,
account_id: &str,
input: DescribeTableInput,
) -> BoxFuture<'_, Result<TableDescription, StorageError>>;
// ...list_tables, update_table, table_key_info
}Duas coisas a notar. Primeiro, cada metodo recebe um account_id. O escopo de conta nao e uma camada acima do armazenamento, ele percorre todo o caminho. Segundo, o trait usa BoxFuture para object safety. Os docs no nivel do modulo mencionam "RPITIT", mas os metodos reais foram implementados como boxed futures, provavelmente para manter o trait object-safe para a camada de engine.
Configuracao do laboratorio
Ambos os backends rodam no mesmo Mac. Postgres 18.4 do Homebrew, iniciado com brew services start postgresql@18. ExtendDB compilado com cargo build --release, depois:
$ ./target/release/extenddb init --config extenddb.toml …criando banco de catalogo extenddb_catalog… …gerando certificado TLS autoassinado em ~/.extenddb/tls/cert.pem… Credenciais de admin (mostradas uma vez, salve-as agora) │ Username: admin │ Password: <redacted>
TLS e obrigatorio. O comando init gera um certificado EC P-256 autoassinado de 587 bytes (openssl x509 -in cert.pem -noout -subject mostra CN=extenddb self-signed, O=extenddb). Os clientes confiam nele via AWS_CA_BUNDLE=~/.extenddb/tls/cert.pem.
O ExtendDB entao roda com extenddb serve em https://127.0.0.1:8000. Credenciais de teste sao provisionadas atraves da API de gerenciamento via o script devtools/provision-test-credentials, que cria uma conta de teste, usuario IAM, chave de acesso e uma politica allow-all-dynamodb.
O DynamoDB Local vai ao lado, em Docker:
$ docker run -d --name ddb-local -p 8001:8000 \
amazon/dynamodb-local:latest \
-jar DynamoDBLocal.jar -inMemory -port 8000Uma fixture conftest do boto3 alterna entre os dois endpoints com base na variavel de ambiente LAB_BACKEND, entao cada teste roda contra qualquer backend sem alteracoes de codigo. Transcricoes de cada teste foram capturadas como linhas JSON. Tudo neste post e citado delas.
Paridade basica: CRUD funciona sem problemas
A primeira coisa a saber e que o facil simplesmente funciona. Um PutItem com todo o zoologico de tipos:
ddb.put_item(TableName="lab_alpha", Item={
"pk": {"S": "user#1"},
"sk": {"S": "profile"},
"name": {"S": "Alice"},
"age": {"N": "30"},
"active": {"BOOL": True},
"tags": {"L": [{"S": "rust"}, {"S": "postgres"}]},
"meta": {"M": {"city": {"S": "Istanbul"}}},
"deleted_at": {"NULL": True},
"blob": {"B": b"\x00\x01\x02"},
})Mesma chamada, ambos os backends, mesmo formato de resposta. Dez testes cobrindo CreateTable (com um GSI), GetItem com consistencia forte, Query em particao + faixa de sort, Query no GSI, Scan com filtro, UpdateItem com SET/REMOVE/ADD combinados mais uma ConditionExpression, DeleteItem com condicao, BatchWriteItem de 25 itens e um TransactWriteItems com um conflito intencional de verificacao de condicao. Todos passam em ambos os backends.
Uma pequena excecao que vale notar. A resposta BatchWriteItem do ExtendDB omite a chave UnprocessedItems completamente quando o lote foi totalmente bem-sucedido. O DynamoDB real e o DynamoDB Local ambos retornam "UnprocessedItems": {}. E o tipo de divergencia que o boto3 contorna com resp.get("UnprocessedItems", {}), mas um cliente rigoroso poderia tropecar nisso.
Entao, no nivel de "codigo boto3 que rodava contra o DynamoDB ontem roda contra o ExtendDB hoje", a resposta e sim. As diferencas interessantes comecam onde o DynamoDB Local historicamente tem sido contornado.
Onde eles divergem
Autenticacao e real, de certa forma
O atalho popular "DynamoDB Local nao tem autenticacao" acaba sendo ligeiramente errado. Ambos os backends rejeitam uma requisicao completamente nao assinada:
# B1: sem cabecalho Authorization em nenhum extenddb -> 400 com.amazon.coral.service#MissingAuthenticationToken dynamodb-local -> 400 com.amazonaws.dynamodb.v20120810#MissingAuthenticationToken
A verdadeira diferenca aparece no B2. Envie qualquer cabecalho Authorization que parse como SigV4 (um access key id inventado, um Signature=deadbeef, qualquer coisa), e:
# B2: cabecalho Authorization falso mas parseavel
extenddb -> 400 UnrecognizedClientException
"The security token included in the request is invalid."
dynamodb-local -> 200 {"TableNames":[]}O ExtendDB consulta a chave de acesso em seu armazenamento IAM e rejeita quando ela nao existe. O DynamoDB Local nem olha para a chave de acesso. Ele aceita a requisicao no momento em que o cabecalho faz parse. Essa e a diferenca.
Detalhe pequeno mas util: as chaves de acesso do ExtendDB sao prefixadas com AKIAEXTENDDB (ou ASIAEXTENDDB para sessoes), entao elas nao podem ser confundidas com chaves AWS reais em logs ou saida de grep.
Streams com o formato real
Ambos os backends implementam Streams. O formato e diferente. Habilite uma stream com NEW_AND_OLD_IMAGES, escreva um item, sobrescreva-o, exclua-o, depois drene a stream:
# Leitura ingenua de shard unico contra ExtendDB retorna zero registros.
# ExtendDB particiona registros de stream em 4 shards por hash da chave de particao.
records = []
for shard in describe_stream(stream_arn)["StreamDescription"]["Shards"]:
it = get_shard_iterator(StreamArn=stream_arn,
ShardId=shard["ShardId"],
ShardIteratorType="TRIM_HORIZON")["ShardIterator"]
# ...drenar paginas ate esvaziarIsso corresponde a como o DynamoDB real particiona streams. O DynamoDB Local adota um caminho mais simples e coloca tudo em um unico shard, e por isso consumidores ingenuos funcionam contra ele e depois surpreendem seus autores em producao.
Uma pegadinha da v0.1.0 que vale destacar. Excluir uma tabela com stream habilitada e recria-la com o mesmo nome durante o mesmo tempo de vida do servidor falha com duplicate key value violates unique constraint "stream_shards_pkey". As linhas de shard da tabela antiga nao sao completamente limpas. A solucao no laboratorio e adicionar timestamp aos nomes das tabelas.
TTL com registros REMOVE reais
O ExtendDB executa um sweeper de TTL. Insira um item ja expirado, espere, e o item desaparece:
ddb.put_item(TableName="lab_ttl_...",
Item={"pk": {"S": "doomed"},
"expires_at": {"N": str(int(time.time()) - 60)},
"payload": {"S": "x"}})
# 60 segundos depois
ddb.get_item(...) # sem ItemMais importante, a exclusao emite um registro de stream com a identidade de servico especificada:
INSERT userIdentity = None
REMOVE userIdentity = {'PrincipalId': 'dynamodb.amazonaws.com', 'Type': 'Service'}Esse userIdentity importa. E o que o codigo de consumo real usa para distinguir um DeleteItem iniciado pelo usuario de uma expiracao TTL em segundo plano. Duas pequenas divergencias aqui. Primeiro, o ExtendDB retorna as chaves como Type / PrincipalId (PascalCase), onde a especificacao do DynamoDB usa type / principalId. Codigo que faz record["userIdentity"]["type"] precisara de um fallback. Segundo, os docs referenciam uma configuracao de runtime ttl_deletion_target_seconds; na v0.1.0 nao e uma chave exposta, e o intervalo de sweep esta hardcoded em 60 s em crates/storage-postgres/src/ttl_worker.rs. O DynamoDB Local pula toda essa historia: sem sweeper nenhum, itens expirados permanecem na tabela para sempre.
Isolamento multi-account que funciona
Provisionada uma segunda conta lab_b, criado um usuario, chave de acesso e uma politica allow-all-dynamodb. Entao, de dois clientes boto3 (um assinado como cada conta), ambos criaram uma tabela com o mesmo nome lab_shared_name e inseriram uma linha marcadora cada:
Conta A le de volta -> {"owner": "from-a", "pk": "x"}
Conta B le de volta -> {"owner": "from-b", "pk": "x"}Mesmo nome de tabela, conta diferente, linhas separadas, sem conversa cruzada. O DynamoDB Local tem exatamente um namespace: mesmo nome, mesma tabela.
Voce pode olhar dentro
O ExtendDB armazena itens em PostgreSQL puro. Ha um banco de dados compartilhado extenddb (o isolamento de conta e aplicado no nivel da linha via account_id, nao por bancos de dados separados). Cada tabela DynamoDB e mapeada para uma tabela Postgres chamada _ddb_<uuid>. Uma linha e pk | item_data (JSONB):
$ psql -d extenddb -c "SELECT * FROM _ddb_f6453e99-3173-... LIMIT 2;"
pk | item_data
---------+------------------------------------------------------------------------------
item-001| {"pk": {"S": "item-001"}, "owner": {"S": "account-a"}, "value": {"N": "42"}}
item-002| {"pk": {"S": "item-002"}, "owner": {"S": "account-a"}, "value": {"N": "99"}}Para backup, restauracao, replicacao, depuracao ou apenas para satisfazer a curiosidade, este e um mundo diferente do arquivo opaco em memoria ou SQLite do DynamoDB Local.
Seguranca contra crash e HTTPS
kill -9 no processo ExtendDB no meio de uma escrita, reinicie, leia o canario de volta:
== Ler canario apos reinicio ==
"Item": { "payload": {"S": "survives-crash"}, "pk": {"S": "canary"} }Durabilidade do Postgres em vez de memoria do processo. O DynamoDB Local no modo -inMemory perde tudo no reinicio por design.
HTTPS e obrigatorio, mas nao e implementado como recusa total. Uma requisicao HTTP simples para a porta do ExtendDB retorna 301 (redirecionamento para HTTPS). Nenhum dado de aplicacao e servido por HTTP simples, mas a conexao e aceita tempo suficiente para enviar o redirecionamento. O DynamoDB Local serve HTTP normalmente.
Duas escolhas de design que valem a pena destacar
A primeira e como as streams permanecem consistentes. De crates/storage/src/lib.rs:
/// Parametros para capturar um registro de stream dentro de uma transacao de escrita de dados.
///
/// Quando presente, o backend de armazenamento insere o registro de stream na mesma
/// transacao que a escrita de dados, garantindo atomicidade.
pub struct StreamCapture {
pub view_type: StreamViewType,
pub user_identity: Option<UserIdentity>,
pub region: Arc<str>,
}A escrita do registro de stream vive dentro da mesma transacao Postgres que a escrita do item. Nao e possivel "dados commitados mas stream perdida" ou "stream escreveu mas dados sofreram rollback". Ambos acontecem, ou nenhum acontece. E o tipo de coisa que o DynamoDB real esconde atras de seu armazenamento proprietario, e e satisfatorio ver a interface explicitamente no trait.
A segunda e uma escolha defensiva pequena e surpreendente. De docs/differences-from-dynamodb.md:
| Nome do atributo TTL | Qualquer string UTF-8 (1 a 255 bytes) | Restrito a
[a-zA-Z0-9._-]+(1 a 255 bytes). Nomes com espacos, aspas ou outros caracteres especiais sao rejeitados. Isso elimina o risco de SQL injection no indice de expressao TTL. |
O DynamoDB permite qualquer UTF-8 no nome do atributo TTL. O ExtendDB rejeita qualquer coisa fora de [a-zA-Z0-9._-]+. A razao e que o nome do atributo TTL e interpolado em uma expressao de indice Postgres, e um conjunto de nomes permissivo forcaria quoting que poderia ser explorado. E uma implementacao do zero fazendo um trade-off de seguranca que o original provavelmente nunca precisou considerar.
Quando NAO usar
Uma lista curta de lacunas concretas, misturando recursos genuinamente ausentes com divergencias da v0.1.0:
- Nao implementado de forma alguma: Global Tables, DAX, PartiQL (
ExecuteStatement/BatchExecuteStatement), autenticacao federada SAML/OIDC, destinos de streaming Kinesis. - Formato diferente:
ImportTable/ExportTableToPointInTimevao para um caminho de sistema de arquivos local em vez de um bucket S3. Capacidade on-demand tem um burst inicial fixo e sem auto-scaling. - Divergencias de protocolo wire para ficar atento:
BatchWriteItemomiteUnprocessedItems: {}de respostas bem-sucedidas. CamposuserIdentityusam chaves PascalCase (Type,PrincipalId) em vez do camelCase da especificacao DynamoDB. Consumidores de stream devem drenar todos os shards (comportamento real do DynamoDB, mas usuarios do DynamoDB Local podem ter apenas lido o shard 0). - Bug da v0.1.0: uma tabela com stream habilitada nao pode ser excluida e recriada com o mesmo nome dentro do mesmo tempo de vida do servidor (colisao
stream_shards_pkey). Rotacione nomes de tabelas nos testes. - Divergencia de docs: a configuracao de runtime
ttl_deletion_target_secondsreferenciada no doc de diferencas nao esta atualmente exposta; o intervalo de sweep esta hardcoded.
Nenhuma dessas e razao para evitar o ExtendDB se voce esta esbarrando nas limitacoes do DynamoDB Local. Sao razoes para testar seu codigo contra o ExtendDB antes de assumir que o wire e identico.
Veredito
Se voce ja escreveu codigo de teste que contornou o DynamoDB Local (mockou autenticacao, pulou assercoes de Streams, stubou TTL) e silenciosamente se preocupou que o servico real se comportaria de forma diferente, o ExtendDB e interessante. E o primeiro substituto local do DynamoDB que leva a serio autenticacao, Streams, TTL, isolamento multi-account e durabilidade, e a camada de armazenamento e Postgres puro no qual voce pode fazer psql.
Para pipelines CI que precisam de semantica realista de autenticacao e stream, implantacoes on-prem ou air-gapped e equipes de dev que tem perdido tempo com "bem, funciona contra o DynamoDB Local...", a v0.1.0 ja e uma ferramenta util. Para qualquer coisa que dependa de compatibilidade wire byte-exata com o servico real, faça sua propria verificacao primeiro.
Links:
- Repo: https://github.com/ExtendDB/extenddb
- Release: https://github.com/ExtendDB/extenddb/releases/tag/v0.1.0
- Getting started: https://github.com/ExtendDB/extenddb/blob/v0.1.0/docs/getting-started.md
- Diferencas comportamentais: https://github.com/ExtendDB/extenddb/blob/v0.1.0/docs/differences-from-dynamodb.md
- Imagem Docker DynamoDB Local: https://hub.docker.com/r/amazon/dynamodb-local
- Docs AWS DynamoDB Local: https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/DynamoDBLocal.html
- Especificacao
userIdentityde stream TTL do DynamoDB: https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/time-to-live-ttl-streams.html
Mais de Ercan
Mais dois sites, mesmo autor, terreno diferente.
IA, LLMs, agentes, ML aplicado.
Notas de campo sobre cargas de IA. Análise de custos do Bedrock, padrões de agentes, trade-offs de armazenamento vetorial, modos de falha em produção.
Visitar ercan.ai →O hub. Sobre, consultoria, contato.
Hub pessoal para as duas trilhas de escrita. Quem sou eu, como funciona a consultoria, como me contatar.
Visitar ercanermis.com →