De Notebooks em Python para Contratos em YAML: Como um framework de ingestão declarativa de PBs de dados acelerou a operação do Data Lake
← Voltar para Artigos

De Notebooks em Python para Contratos em YAML: Como um framework de ingestão declarativa de PBs de dados acelerou a operação do Data Lake

TL;DR

  1. Colocamos em produção uma stack declarativa de ingestão para o Data Lake baseada em contratos YAML.
  2. Hoje operamos uma quantidade massiva de dados com cerca de 7 PB de dados, ~8.000 tabelas transacionais e ~850 YAMLs declarativos.
  3. Saímos de um modelo espalhado via implementações locais para outro com 1 tabela : 1 YAML e 2 notebooks centrais.
  4. O novo fluxo já cobre cerca de 85% do caminho Source → Bronze → Silver.
  5. O tempo estimado para colocar uma nova ingestão no ar caiu de dias para horas.

O Problema de Escala que Virou Problema de Arquitetura

Durante muito tempo, o problema não era colocar dado no Data Lake. O problema era crescer sem transformar cada nova ingestão em mais custo estrutural.

Hoje, a CERC opera uma plataforma com cerca de 7 PB de dados e ~8.000 tabelas transacionais. Nessa escala, ingestão deixa de ser script. Ela vira infraestrutura de plataforma.

Enquanto a operação era menor, o modelo antigo parecia aceitável. Cada domínio criava seus próprios notebooks, seus próprios padrões e, em alguns casos, seu próprio repositório. Isso dava liberdade local. Também criava divergência estrutural.

Com o tempo, a conta apareceu. O esforço de manutenção passou a crescer mais rápido do que o valor entregue por cada nova fonte. O custo real não estava só em compute. Estava no tempo de engenharia gasto repetindo estrutura, revisando variações da mesma ideia e reconstruindo contexto a cada nova ingestão.

Esse problema ficava mais visível no fluxo Source → Bronze → Silver, que concentra uma parte grande da superfície operacional do Data Lake. Nesse trecho, pequenas diferenças de implementação viravam mais revisão, mais manutenção e menos velocidade.

As dores apareciam em quatro frentes:

Código repetido demais

Cada nova ingestão repetia a mesma base estrutural, com variações difíceis de governar.

Velocidade baixa

Criar uma fonte nova levava dias, porque o trabalho era implementar pipeline, não declarar ingestão.

Governança fraca

O padrão esperado nem sempre era o padrão executado, porque cada implementação tinha liberdade demais.

Custo cognitivo alto

Cada mudança exigia entender decisões locais antes de mexer em qualquer coisa.

Não era mais uma questão de estilo. Era uma questão de operabilidade.


A Mudança de Modelo

Não bastava reduzir o número de notebooks. Precisávamos trocar o paradigma de desenvolvimento da ingestão.

O objetivo era sair de um modelo em que cada time descrevia como executar a ingestão para outro em que o time declarasse o que precisava ser ingerido, e a plataforma cuidasse do resto.

Na prática, isso significava centralizar no núcleo da stack o que antes ficava espalhado: validação de contrato, resolução de ambiente, publicação em Bronze e Silver, tratamento de deletes e regras de schema.

Os critérios eram diretos:

  1. Padronizar a maior parte dos workflows sem abrir espaço demais para exceções estruturais.
  2. Reduzir a superfície de manutenção da plataforma.
  3. Acelerar a entrada de novas fontes no Data Lake.
  4. Fortalecer governança sem transformar o time de plataforma em gargalo manual.

Quando formulamos o problema desse jeito, a decisão ficou clara. O gargalo não estava na falta de notebooks. Estava no excesso de liberdade estrutural.


O Contrato Declarativo

A filosofia da nova stack pode ser resumida em uma frase: tornar a coisa certa a coisa fácil.

Uma nova ingestão deixou de começar com um notebook Python. Ela passou a começar com um contrato YAML. Esse contrato descreve metadados, origem, destino, schema e regras de publicação. O YAML virou a interface humana da plataforma. O runtime continuou como código reutilizável.

Em linhas gerais, uma ingestão segue este padrão/template:

metadata:
  table_description: "Descrição funcional da tabela"
  table_source_owner: "time-dono-da-fonte"
  table_datalake_owner: "time-dono-do-datalake"
  ingestion_type: batch
  ingestion_mode: full

workflow:
  name: fonte-bronze-silver-nome-da-tabela
  schedule_america_sp: "25 03 * * *"

ingestion:
  bronze:
    source:
      prd:
        format: cloud-spanner
        dynamic_configs:
          project_id: "projeto-prd"
          instance_id: "instancia-origem"
          database_id: "database-origem"
          table: "nome_da_tabela_origem"
    destination:
      format: parquet
      unity:
        schema_unity: "dominio_bronze"
        table_unity: "nome_da_tabela_bronze"

  silver:
    destination:
      format: delta
      unity:
        schema_unity: "dominio_silver"
        table_unity: "TB_NOME_DA_TABELA_SILVER"
    schema_config:
      partition_by: ["CuratedDt"]
      columns:
        - source_name: source_id
          silver_name: Id
          datatype: STRING
          primary_key: true
        - source_name: data_operacao
          silver_name: DataOperacao
          datatype: DATE
          primary_key: false
        - source_name: valor_financeiro
          silver_name: ValorFinanceiro
          datatype: FLOAT
          primary_key: false
        - source_name: data_pagamento
          silver_name: DataPagamento
          datatype: DATE
          primary_key: false

O ponto importante é este: o YAML não descreve só o nome da tabela. Ele descreve o contrato de ingestão de uma tabela.

No modelo novo, essa é a unidade principal de autoria: 1 tabela : 1 YAML. O engenheiro descreve a ingestão. A plataforma decide como executá-la.


Como a Stack Executa o Contrato

O YAML não vai direto para produção. Antes disso, a stack valida o contrato e o transforma em parâmetros válidos de execução.

Na prática, o fluxo segue esta ordem:

  1. Um engenheiro cria ou atualiza uma spec YAML.
  2. A spec passa por validação estrutural e semântica.
  3. A plataforma transforma a spec em parâmetros de execução carregando o YAML como um dicionário em runtime.
  4. Dois notebooks centrais executam o contrato em Bronze e Silver com parâmetros do item 3.
  5. A ingestão acontece com caminhos, formatos e regras padronizadas dependendo dos parâmetros extraídos do YAML.

Esse desenho reduz um erro clássico de plataforma: o pipeline funciona, mas cada time o implementa de um jeito.

No núcleo do runtime, a divisão é simples:

  1. O notebook de Bronze lê a origem e escreve os dados no caminho padronizado no bucket do Google Cloud Storage na bronze.
  2. O notebook de Silver lê a Bronze (o bucket do Google Cloud Storage na bronze), aplica schema, casting, deduplicação e publica a tabela final no bucket do Google Cloud Storage na silver.

Essa centralização muda a economia da manutenção. Quando uma regra estrutural evolui, ela evolui em um núcleo comum, não em centenas de notebooks quase iguais.


Governança e Operação no Centro da Stack

Uma parte importante dessa história não está no YAML. Está no que impede o YAML de virar bagunça.

Antes de qualquer execução, a spec passa por uma camada de validação com Pydantic. Essa camada verifica formato aceito de source, presença de campos obrigatórios, coerência entre campos, consistência por ambiente e regras de schema.

Na prática, a governança aparece em mecanismos concretos:

  1. Campos obrigatórios e enums bloqueiam configurações inválidas logo na entrada.
  2. Allowlists garantem que projetos, formatos e certos comportamentos sigam convenções conhecidas.
  3. Guardrails impedem usos perigosos, como casos de método de escrita overwrite fora do fluxo aprovado.
  4. Regras cruzadas validam coerência entre modo de ingestão e filtro configurado.
  5. Ownership e metadados deixam explícito quem é dono da origem e quem é dono da tabela no Data Lake.

Esse é o ponto em que a stack troca liberdade por operabilidade. Convenção deixa de ser recomendação. Ela vira critério de entrada.

Essa camada também faz a stack ir além de “copiar dado”. O runtime já incorpora validação, data quality e controles operacionais que antes ficavam espalhados por implementações locais.


GhostBuster: Deletes Viraram Fluxo de Plataforma

O GhostBuster é o mecanismo da stack que garante que exclusões feitas na origem transacional sejam refletidas corretamente na camada silver do Data Lake.

No contrato declarativo, esse comportamento pode ser habilitado na própria spec YAML. A partir daí, delete deixa de ser exceção tratada caso a caso em cada tabela e passa a fazer parte da operação padrão da plataforma.

No dia a dia, isso muda a ingestão em quatro pontos:

  1. A tabela já nasce com uma regra explícita de tratamento de exclusões.
  2. Em reprocessamentos, a stack evita que registros já removidos na origem voltem a aparecer na silver.
  3. Quando a validação encontra IDs pendentes de remoção, o caso entra em um fluxo controlado de deleção.
  4. Esse fluxo fica registrado em uma trilha operacional até a execução do hard delete.

O efeito prático foi reduzir um tipo recorrente de atrito operacional. Antes, deletes na silver costumavam abrir demandas manuais e estender a janela de inconsistência entre origem e Data Lake. Agora, boa parte desse trabalho é absorvida pela própria stack.


Streaming: O Mesmo Contrato, Outro Ritmo

Batch e streaming costumam ser tratados como mundos separados. Pipelines diferentes, ferramentas diferentes, lógicas diferentes. Na stack declarativa, o contrato YAML é o mesmo. A diferença está em um campo: ingestion_type: streaming.

A partir daí, a plataforma executa o fluxo correto. O engenheiro declara a ingestão. A stack decide como processá-la.

Fonte: Google Cloud Pub/Sub

No caso de streaming, a principal fonte que operamos é o Google Cloud Pub/Sub. Em vez de ler tabelas transacionais por polling, a stack consome mensagens publicadas em um tópico. Cada mensagem carrega um payload em binário que a plataforma persiste na camada Bronze antes de qualquer transformação.

O caminho é análogo ao batch, mas adaptado para o modelo orientado a eventos:

Pub/Sub Bronze (Parquet) Silver (Delta)

Dois Notebooks Centrais (de Novo)

Assim como no batch, o runtime de streaming é centralizado. Não há um notebook por tópico. Há dois notebooks centrais que a plataforma instancia com os parâmetros extraídos do contrato YAML:

  • Bronze Streaming: lê o tópico Pub/Sub via Structured Streaming do Apache Spark e persiste os dados na camada Bronze no formato Delta, com partição por data de ingestão.
  • Silver Streaming: lê a tabela Bronze de streaming, aplica renomeação de colunas, casting, trimming e colunas calculadas, e publica o resultado na camada Silver.

A mesma lógica de centralização do batch se aplica aqui. Uma mudança no runtime impacta todos os contratos de streaming de uma vez.

O Contrato YAML de Streaming

A diferença entre um YAML de batch e um de streaming está em três pontos: o campo ingestion_type, o formato da fonte (pubsub) e um bloco streaming que define o checkpoint e o modo de trigger.

metadata:
  table_description: "Descrição funcional da tabela de streaming"
  table_source_owner: "time-dono-da-fonte"
  table_datalake_owner: "time-dono-do-datalake"
  ingestion_type: streaming
  ingestion_mode: incremental

workflow:
  name: streaming-Bronze-Silver-nome-da-tabela
  schedule_america_sp: "*/30 * * * *"

ingestion:
  bronze:
    source:
      prd:
        format: pubsub
        dynamic_configs:
          project_id: "projeto-prd"
          subscription_id: "nome-da-subscription"
          topic_id: "nome-do-topico"
          max_records_per_fetch: 10000
    destination:
      format: delta
      unity:
        schema_unity: "dominio_Bronze"
        table_unity: "tb_nome_da_tabela_Bronze"
        partition_by:
          - "dt_ingestion"
      destination_columns_schema:
        messageId: "string"
        payload: "binary"
        dt_ingestion: "date"
      streaming:
        trigger:
          available_now: true
        check_point_location: "gs://bucket-checkpoints/Bronze/dominio/tabela"

  silver:
    streaming:
      trigger:
        available_now: true
    destination:
      format: delta
      unity:
        schema_unity: "dominio_Silver"
        table_unity: "TB_NOME_DA_TABELA_Silver"
    schema_config:
      partition_by:
        - "CuratedDt"
      columns:
        - source_name: messageId
          Silver_name: MessageId
          datatype: string
          primary_key: true

Trigger available_now: true

O modo padrão que operamos é o available_now: true. Ele instrui o Spark Structured Streaming a processar todos os dados disponíveis no momento da execução e encerrar o job. O comportamento é parecido com um micro-batch controlado: consome o que está na fila, finaliza, libera o cluster.

Esse modo funciona bem com schedulers (Airflow, por exemplo), porque o job tem início e fim previsíveis, sem precisar de um cluster dedicado rodando continuamente.

Checkpoint: Gerenciado pelo Contrato

O checkpoint location é o mecanismo que garante que o Spark Structured Streaming saiba exatamente de onde retomar o processamento após uma falha ou reinicialização. No contrato YAML, ele pode ser declarado explicitamente ou deixado para a plataforma gerar automaticamente a partir do nome da tabela e do ambiente:

gs://bucket-checkpoints/{env}/streaming_checkpoints/Silver/{schema}/{tabela}

Quando o checkpoint não é informado no YAML, a plataforma preenche esse caminho automaticamente. Isso evita que checkpoints sejam perdidos por esquecimento ou por configuração manual inconsistente.

A Mesma Governança

O bloco streaming passa pelas mesmas validações Pydantic que o restante do contrato. Campos obrigatórios são verificados, formatos de path são validados e a consistência entre ambientes é garantida antes de qualquer execução. A plataforma não abre exceções estruturais para streaming: o modelo declarativo é o mesmo.


Adoção em Escala de IA Generativa

A stack virou o padrão operacional da ingestão quando o contrato declarativo passou a ser a unidade principal de autoria da plataforma.

Hoje, operamos com cerca de 850 YAMLs em produção. Esse número importa menos pelo volume em si e mais pelo que ele prova: a stack deixou de ser um padrão novo e virou o padrão operacional da ingestão.

Usamos agentes de IA para acelerar a parte mais repetitiva da migração, como criação e atualização de specs. Eles reduziram trabalho mecânico, mas não mudaram a lógica central do desenho. O ganho estrutural veio da stack declarativa. O repositório conta com diversas skills, instructions e prompts para os agentes auxiliarem na criação e evolução dos YAMLs levendo em horas o que antes levava dias.

Migração: De 530 Notebooks para 530 YAMLs

Essa mudança não aconteceu em um espaço vazio. Cerca de 530 notebooks legados precisaram ser convertidos para o novo contrato declarativo. Essa migração foi o passo necessário para trocar o modelo antigo por um fluxo em que a plataforma consegue evoluir em um núcleo comum.

Agentes de IA nos ajudaram em todo o processo de migração, desde a identificação de notebooks candidatos até a criação inicial dos YAMLs.

O importante não era só converter o código. Era converter a lógica de cada ingestão para o modelo declarativo, o que exigiu decisões de modelagem e ajustes para casos especiais. O resultado foi uma migração mais rápida e consistente, que deixou a stack pronta para operar em escala com o novo modelo.

Migrar 530 notebooks para 530 YAMLs não foi só uma questão de volume. Foi uma questão de transformar a forma como a ingestão é pensada, escrita e mantida. O contrato declarativo virou o novo centro da operação, e a migração foi o passo necessário para chegar lá.

Dados Públicos: Cobertura Completa em Outro Repositório

O modelo de cobertura com ativos de IA não se limita à stack declarativa. O repositório de ingestão de dados públicos brasileiros — CGU, CVM, IBGE, Receita Federal, IBAMA, entre outros — também está completamente coberto.

Lá, os engenheiros não escrevem contratos YAML para descrever pipelines. O padrão é diferente: cada fonte tem um notebook Databricks que lê a origem pública, gera um ID único por registro e grava os dados no Google Cloud Storage. O que é igual é a filosofia: tornar a coisa certa a coisa fácil.

No momento em que escrevemos esse artigo, o repositório está coberto com cinco tipos de ativos Copilot:

  1. 1 agente especialista (black-belt.agent.md) com contexto completo do repositório.
  2. 5 skills cobrindo os cenários mais comuns: estrutura de notebook, interação com GCS, download multithread, descoberta de chave primária e configuração de workflow YAML.
  3. 4 instruction files com padrões obrigatórios de código, nomenclatura e organização.
  4. 3 prompts para tarefas recorrentes: adicionar uma nova fonte, modificar uma ingestão existente e diagnosticar um workflow com problema.

Com esses ativos, um agente consegue criar um notebook completo para uma nova fonte pública — com retry, logging, geração de ID e upload para GCS — sem precisar de orientação manual a cada passo.

Uma Skill em Ação: Descoberta da Chave Primária

Dados públicos raramente têm um ID único garantido na fonte. Um arquivo da Receita Federal não tem UUID. Um dataset do IBGE não tem chave primária explícita. Sem um ID por registro, deduplicação e rastreabilidade ficam comprometidas.

A skill primary-key-discovery resolve esse problema com uma árvore de decisão. Antes de decidir, o agente verifica cerca de 200 linhas de dados reais da fonte. Essa amostra determina a estratégia de ID antes de qualquer código ser escrito.

A skill também define o que não fazer: MD5 para chaves de registro (risco de colisão), campos mutáveis no hash (status, contadores), e timestamp como único identificador. Essas regras estão no arquivo da skill. O agente as aplica automaticamente.

O resultado é que cada nova fonte de dados públicos já nasce com uma(s) PK(s) rastreável(eis), validada e consistente com todas as outras. Sem instrução manual. Sem revisão caso a caso.


O que a Stack Cobre Hoje

A stack declarativa hoje governa cerca de 850 YAMLs em produção e cobre aproximadamente 85% dos workflows do fluxo Source → Bronze → Silver.

Dentro desse caminho principal, a stack já padroniza:

  1. O fluxo principal de batch.
  2. Suporte a múltiplos formatos de origem, incluindo Spanner, BigQuery, Delta e arquivos.
  3. Configuração explícita por ambiente, com stg, int e prd tratados como parte do contrato.
  4. Streaming via Google Cloud Pub/Sub com Spark Structured Streaming, usando o mesmo modelo declarativo descrito acima.

Isso importa porque mostra o limite real do modelo. A stack cobre a maior parte da operação sem fingir que todo caso especial cabe no mesmo caminho. O ganho está em padronizar o que é recorrente e deixar explícito onde a borda começa.

E a Sustentação?

A stack declarativa eliminou a necessidade de uma grande parte da sustentação. Ela mudou o tipo de sustentação que fazemos. Por um lado antes cada notebook podia ser um caso diferente. Por outro lado, agora temos um núcleo comum para evoluir e melhorar. A sustentação hoje é mais focada em evoluir o runtime, melhorar a camada de validação e garantir que o contrato continue sendo a interface humana da plataforma. O ganho é que, quando fazemos uma melhoria estrutural, ela impacta toda a stack, não só um caso específico.

Colocar uma coluna nova vindo de uma migração transacional, por exemplo, não é mais um caso de notebook. É uma evolução do contrato que pode ser aplicada em centenas de YAMLs com o mesmo ajuste. O resultado é que a sustentação evolui de um trabalho de manutenção reativa para um trabalho de evolução proativa da plataforma.

Alie isso a Agente de IA e temos um cenário em que a sustentação é mais rápida, mais consistente e mais focada em evoluir a plataforma do que em manter casos específicos. O contrato declarativo virou o centro da operação, e a sustentação virou o centro da evolução da plataforma.

Qualquer um pode criar uma nova ingestão?

Sim. Essa é a ideia. O modelo declarativo e a camada de validação foram desenhados para que qualquer engenheiro possa criar uma nova ingestão seguindo o contrato. A governança é garantida pela validação, que bloqueia configurações inválidas ou perigosas. O resultado é que a criação de novas ingestões se torna mais self-service, sem depender de um time central de plataforma para cada nova fonte. O contrato declarativo é a interface humana da plataforma, e ele foi desenhado para ser acessível e fácil de usar, mesmo para quem não tem experiência prévia com a stack. O objetivo é democratizar a criação de ingestões, mantendo a governança e a operabilidade da plataforma.

Times internos já começaram a fazer PRs de criação de novas ingestões seguindo o modelo declarativo, e a resposta tem sido positiva. O processo é mais rápido, mais previsível e menos propenso a erros do que o modelo anterior. O contrato declarativo virou o novo padrão para criar ingestões, e a plataforma está pronta para escalar com esse modelo. O resultado é que, com o contrato declarativo, a plataforma pode crescer de forma mais rápida e consistente, sem repetir os custos estruturais do passado.

Um exemplo muito comum é a criação de ingestões de tabelas públicas que times as encontram e desejam colocar no Data Lake. Com o modelo declarativo, eles podem criar um YAML seguindo o contrato, e a plataforma cuida do resto. O resultado é que a entrada de novas fontes se torna mais rápida e menos dependente de intervenção manual, o que acelera o crescimento do Data Lake sem comprometer a governança ou a operabilidade.


Os Resultados

A tabela abaixo resume o que mudou no modelo de desenvolvimento e operação:

AspectoAntesDepois
Paradigma de desenvolvimentoImperativo, focado no “como”Declarativo, focado no “o que”
Superfície principal de autoriaNotebooks Python, no modelo 2 notebooks : 1 tabela bronze e 1 tabela silverYAMLs declarativos, no modelo 1 YAML : 1 tabela bronze e 1 tabela silver
Tempo estimado para nova ingestãoDias por nova fonteHoras por nova fonte
Escala atual da stackLógica espalhada por implementações de notebooks isolados~850 YAMLs centralizados
Núcleo de execuçãoImplementações distribuídas2 notebooks centrais
GovernançaVariava por implementaçãoValidada por contrato
Tratamento de deletesSoluções locais e intervenção manualGhostBuster com fluxo padronizado e rastreável
OrganizaçãoMúltiplos padrões locaisModelo unificado de ingestão

Quando a autoria da ingestão sai de centenas de implementações livres e vai para contratos validados, a plataforma reduz drasticamente os pontos onde ela pode divergir de si mesma.

Esse ganho aparece em quatro planos ao mesmo tempo:

  1. Menos código repetido para escrever e revisar.
  2. Menos variação estrutural entre workflows.
  3. Mais previsibilidade na operação.
  4. Mais velocidade para colocar fontes novas no ar.

O que Aprendemos

Essa não foi uma troca sem atrito. A simplificação valeu a pena, mas trouxe aprendizados importantes.

1. Adotar um modelo declarativo exigiu mudança de autoria.

Padronizar a tecnologia foi a parte mais direta. Mais difícil foi alinhar a mudança de autoria. Times acostumados a construir a ingestão inteira precisaram passar para um fluxo em que a principal decisão deixa de ser o notebook e passa a ser o contrato.

2. Nem todo workflow entra no modelo novo no mesmo ritmo.

A cobertura de 85% já representa um avanço grande. Ela também mostrou que o contrato precisa ter um limite claro. Quando a exceção vira regra, a stack perde poder de padronização.

3. Simplificar a implementação não elimina a necessidade de boa modelagem.

O modelo declarativo reduz o custo da implementação. Ele não elimina a necessidade de decisões corretas sobre schema, origem, deduplicação, deletes e publicação. Quando o contrato nasce mal modelado, a stack só escala o erro mais rápido.


O que Vem a Seguir

Com 850 YAMLs em produção, a próxima fase é expandir as capacidades da plataforma para novos casos de uso e integrações.

  1. Expandir a cobertura para além dos 85% atuais.
  2. Evoluir a autoria assistida por IA para reduzir o trabalho manual na criação e evolução de specs.
  3. Ampliar conectores, formatos e casos especiais dentro do mesmo modelo declarativo.
  4. Tornar a criação de novas ingestões cada vez mais self-service para os times.
  5. Coletar e extrair mais tabelas transacionais para o Data Lake, acelerando a entrada de novas fontes.

O ponto importante é que a fundação mudou. Agora temos uma base mais simples para crescer sem repetir os custos estruturais do passado.


Tecnologias

CamadaTecnologia
Especificação de ingestãoYAML
ProcessamentoDatabricks + Apache Spark
Camada BronzeNotebook genérico centralizado
Camada SilverNotebook genérico centralizado
Validação e governançaPython + models declarativos + allowlists
Deletes e controle operacionalGhostBuster + Validator + Data Quality
Aceleração de criaçãoAgentes de IA + Asset Inventory + validação automatizada
Organização da stackRepositório unificado de ingestão

A CERC opera a infraestrutura do mercado financeiro brasileiro para registro de recebíveis. Construir plataformas de dados nesse contexto significa trabalhar com escala real, impacto real e decisões de engenharia que precisam ser operáveis no dia seguinte. Se você quer trabalhar em problemas como este, estamos contratando.


Este post foi escrito pelo time de Engenharia de Dados da CERC: Davi Campos, André Tayer e Guilherme Oliveira.