ANM Data API
Um serviço que confere toda manhã 15 bases públicas da Agência Nacional de Mineração (ANM), copia as que mudaram para pastas datadas no AWS S3, entrega os arquivos por uma API REST e avisa os 14 serviços de banco de dados que carregam essas bases. Ele substituiu um ETL antigo em Python.
Projetei e desenvolvi o serviço (291 dos 295 commits são meus) e escrevi o documento de passagem e o runbook.
anm-data-api
- origem
- ANM · CSV · ZIP · SHP · ODS
- horários
- 07:00 08:00 09:00 10:00 11:00 (BRT)
- bases
- 15
- destino
- AWS S3
- api
- REST · CSV -> JSON · paginação
- avisos
- 14 serviços de banco de dados
O problema
Os bancos de dados e as ferramentas de mapa da ÍGNEA partem de dados públicos da Agência Nacional de Mineração (ANM). A agência publica esses dados no portal de dados abertos em pastas de arquivos CSV, ZIP, shapefile e ODS, atualiza tudo entre 06:00 e 06:30 e pode levar minutos para começar a enviar um arquivo grande. Em 2026 o portal ainda mudou de domínio, e o endereço antigo tinha data para sair do ar. A ÍGNEA precisava de um serviço que baixasse cada base uma vez, guardasse uma cópia datada e avisasse o banco certo quando chegasse uma versão nova.
Decisões
Um catálogo, três rotinas
Descrevi cada base como configuração e escrevi uma rotina por formato (ZIP, pasta de CSV, shapefile) em vez de um script por base. Base nova virou mudança de configuração. O custo é encaixar as particularidades de cada base em poucos campos, como lista de arquivos permitidos e extensões bloqueadas.
Aviso depois do upload, cron como reserva
A API avisa cada serviço de banco logo depois que os arquivos chegam ao S3, e os bancos carregam o dado novo sem ficar consultando. O banco responde na hora e faz a carga em segundo plano, um aviso que falha ganha uma nova tentativa e nunca trava a onda, e cada serviço de banco mantém a própria sincronização diária agendada. Um aviso perdido atrasa a carga, mas a atualização não se perde.
Tirar as camadas de mapa da API
Primeiro converti os shapefiles do SIGMINE para GeoJSON dentro deste serviço, com proj4, e servia arquivos estáticos do S3. Isso fugia do padrão das outras bases, então levei as oito camadas para um serviço próprio com PostGIS e deixei a API como proxy com as mesmas URLs, sem exigir mudança nos clientes de mapa.
O que eu fiz
- Um catálogo das bases em TypeScript. Incluir uma base nova é adicionar uma entrada com caminho no portal, formato, lista de arquivos permitidos ou extensões bloqueadas, prefixo no S3 e hora de execução, e mais uma linha no mapa de avisos quando algum banco consome aquela base.
- Extração de ZIP em streaming, do S3 de volta para o S3, com unzipper e o lib-storage do AWS SDK, e um timer que aborta o upload quando ele para de avançar.
- Uma checagem de manifesto para o pacote de microdados do SCM, criada depois que uma execução travou no meio da extração e publicou 10 dos 28 arquivos. Agora a sincronização falha se faltar qualquer um dos 28 arquivos esperados, os bancos não recebem aviso de uma execução que falhou ou foi pulada, e uma variável de ambiente desliga a checagem em caso de rollback de emergência.
- Um serviço de avisos com Axios: um POST por banco com timeout de 15 segundos, uma nova tentativa depois de 30 segundos, uma linha de log por tentativa e uma contagem de sucessos por onda. Ele nunca lança erro, então um serviço fora do ar não interrompe o resto da onda.
- Um leitor de ODS (adm-zip e leitura do XML da planilha) que abre as planilhas de metadados da ANM e publica a descrição dos campos de cada base como JSON no S3. Ele roda depois da sincronização e tem os erros capturados, então uma planilha quebrada não derruba um download.
- A API REST em Express: Helmet, limite de 100 requisições por minuto, chaves de API para downloads e disparos manuais e cache de uma hora com NodeCache nas listagens do portal. O Terraform guarda as regras de ciclo de vida do S3, que movem os arquivos para o Glacier depois de 90 dias e para o Deep Archive depois de um ano.
Como funciona
Um catálogo em TypeScript descreve as 15 bases: a pasta no portal, o formato (um pacote ZIP, uma pasta de CSVs ou ZIPs de shapefile), quais arquivos manter, o prefixo no S3 e a hora de execução. O node-cron agrupa as bases em ondas às 07:00, 08:00, 09:00, 10:00 e 11:00 (horário de Brasília), depois da atualização matinal da ANM. Cada job lê a listagem do diretório no portal, compara as datas de Last-Modified com o histórico de sincronização da base e pula o que não mudou.
Os downloads vão do portal para o S3 em streaming, sem gravar nada em disco. O pacote ZIP do SCM é guardado do jeito que veio, depois lido de volta do S3 e extraído arquivo por arquivo com upload multipart. Tudo fica em prefixos datados, e um JSON de histórico por base guarda as últimas 30 execuções. Depois da sincronização, uma etapa separada transforma as planilhas ODS de metadados da ANM em JSON com nome, tipo e descrição de cada campo.
Quando uma base termina, o agendador faz um POST para cada serviço de banco que usa aquela base. São 14 serviços, e cada um carrega os arquivos do S3 no PostgreSQL. O pacote do SCM alimenta dois deles, e cada uma das oito camadas do SIGMINE manda o próprio ID de camada. O mesmo app Express entrega os arquivos guardados por uma API REST e repassa as requisições de GeoJSON do SIGMINE para o serviço com PostGIS.
Tecnologias
- Back-end
- TypeScript, Node.js, Express, node-cron, Axios, unzipper, adm-zip, csv-parser, NodeCache
- Segurança
- Helmet, express-rate-limit, CORS, chaves de API
- Armazenamento
- AWS S3 (SDK v3, uploads multipart com lib-storage), regras de ciclo de vida para S3 Glacier e Deep Archive
- Infraestrutura
- AWS Lightsail, PM2, Terraform
- Serviços consumidores
- PostgreSQL, PostGIS, Fastify