SELECTSELECT

SELECT

Snowflake DCM Projects: infraestrutura como código, em SQL puro

Esta página também está disponível em English, Deutsch, Español, Français, Italiano e 日本語.

By SELECTNov 12, 2022 min read

TLDR

  • Os DCM Projects são a forma nativa e declarativa do Snowflake de gerenciar objetos como código. Você escreve DEFINE <objeto> em vez de CREATE <objeto> (por exemplo, DEFINE DATABASE), e o Snowflake descobre o que precisa mudar.
  • O ciclo é: editar as definições, planejar e depois fazer o deploy. O plano é um diff somente leitura. O deploy aplica as mudanças.
  • O DCM remove o que você deixa de definir. Apague uma linha e perca um banco de dados. Leia o plano.
  • Um manifest.yml mais Jinja te dá DEV, QA e PROD a partir de um único conjunto de arquivos.
  • O DCM pode gerenciar tabelas e views, mas, na minha opinião, não deveria quando você já usa dbt. Deixe o DCM cuidar da plataforma (roles, bancos de dados, warehouses, grants) e deixe o dbt cuidar do que está dentro dos bancos de dados.
  • O design de roles e bancos de dados vem do guia de configuração de Snowflake da dbt Labs, que centenas de projetos dbt já seguiram. O repositório traz algumas melhorias e transforma tudo em código.
  • Aqui está o repositório no GitHub em que este artigo se baseia, com CI/CD via GitHub Actions que nunca toca no ACCOUNTADMIN.

Como chegamos até aqui

Se você usa Snowflake há algum tempo, provavelmente já gerenciou a infraestrutura da sua conta de uma destas três formas:

  1. Click-ops. Alguém com ACCOUNTADMIN cria um objeto no Snowsight numa terça-feira. Ninguém lembra por quê. Três anos depois, ninguém tem coragem de removê-lo.
  2. Scripts de migração: V1__create_roles.sql, V2__grant_things.sql, V47__fix_the_grant_from_V2.sql. Ferramentas como o schemachange executam tudo em ordem. Funciona, mas o "estado atual" da sua conta é a soma de 47 arquivos — e boa sorte para ler isso.
  3. Terraform. Declarativo e poderoso. Mas também traz HCL, um provider para manter atualizado e um arquivo de estado que você precisa armazenar, travar e, de vez em quando, submeter a uma cirurgia. Para muitos times de dados, isso é uma disciplina inteiramente nova só para criar um warehouse.

Os DCM Projects são a resposta do Snowflake para isso. Você tem o modelo declarativo do Terraform, mas a linguagem é SQL e o estado vive no Snowflake.

O blueprint: os grant statements que todo mundo copiou

Antes de entrarmos no DCM, crédito a quem merece. Se você já configurou o Snowflake para dbt, há uma boa chance de ter lido o post da Claire Carroll, Setting up Snowflake — the exact grant statements we run, no Discourse do dbt. Centenas de projetos dbt foram configurados seguindo aquele roteiro. Eu mesmo já segui mais vezes do que consigo contar.

O design é simples, e é por isso que funciona:

  • Um banco de dados raw para os dados que chegam e um banco analytics para os dados modelados.
  • Uma role loader que escreve em raw, uma role transformer que lê raw e constrói analytics, e uma role reporter que lê analytics e nada mais.
  • Future grants, para que novos schemas e tabelas fiquem legíveis sem ninguém precisar mexer um dedo.

O problema nunca foi o design. O problema é que é uma lista de statements que você cola em uma worksheet uma única vez. Seis meses depois, ninguém sabe dizer se a conta ainda corresponde a ela, e a extensão de "ambiente de dev" no fim do post fica como exercício para o leitor.

Então o repositório inicial mantém o formato da Claire e muda algumas coisas:

Por que inherited grants substituem future grants

Essa linha dos grants é a mudança que mais importa, então vamos nos aprofundar.

Future grants foram a ferramenta certa por muito tempo! São eles que tornam a configuração da Claire de baixa manutenção. Mas eles têm três problemas, e quem roda Snowflake há alguns anos já esbarrou em pelo menos um:

  1. Eles só cobrem o futuro. Um future grant não faz nada por tabelas que já existem, então toda configuração o combina com um statement grant select on all tables. São dois statements por privilégio e duas coisas para lembrar.
  2. Future grants no nível do schema sobrepõem os do nível do banco de dados. Se alguém adicionar um future grant em um schema, o Snowflake ignora os future grants no nível do banco para aquele schema. Sua role reporter silenciosamente deixa de receber novas tabelas ali, e nada dá erro.
  3. Eles são uma cópia pontual, não uma regra. Um future grant é aplicado a cada objeto no momento em que o objeto é criado. Revogue ou altere o future grant depois, e todo objeto que ele já tocou mantém o privilégio antigo. O grant que você enxerga não descreve mais o acesso que as pessoas realmente têm.

Um inherited grant é um único statement em um contêiner (a conta, um banco de dados ou um schema) e se aplica a todo objeto correspondente dentro dele, existente ou futuro:

GRANT INHERITED SELECT ON ALL TABLES IN DATABASE {{ env }}_ANALYTICS
TO ROLE {{ env }}_ANALYST;

Isso substitui, de uma vez, o future grant, o grant on all e a versão por schema de cada um. Quando você quiser saber por que alguém consegue ler uma tabela, o SHOW GRANTS tem as colunas IS_INHERITED e INHERITED_FROM, que apontam para o grant responsável.

Também é um encaixe natural para o DCM. Uma ferramenta declarativa quer uma única linha que declare a regra. "Analistas podem ler tudo em ANALYTICS" agora é uma linha, não seis.

Alguns trade-offs para conhecer:

  • Sem exceções. Você não pode revogar o privilégio em uma tabela específica para abrir uma exceção. A documentação do Snowflake avisa que o revoke parece ter sucesso enquanto a role mantém o acesso por meio do inherited grant. Se uma tabela precisa de acesso diferente, ela pertence a outro schema ou banco de dados.
  • Mover ou clonar um objeto para dentro de um contêiner muda quem pode vê-lo, sem nenhum statement de GRANT em lugar algum. Esse é justamente o objetivo, mas seja intencional com os clones.
  • OWNERSHIP não pode ser herdado. A propriedade continua sendo um grant comum.
  • É preciso uma flag na conta, FEATURE_RBAC_INHERITED_GRANTS, que só o ACCOUNTADMIN pode definir. Falo mais sobre isso no bootstrap abaixo.

O que é um DCM Project no Snowflake?

Duas coisas:

  1. Uma pasta de arquivos. Um manifest.yml e alguns arquivos .sql cheios de statements DEFINE.
  2. Um objeto DCM project no Snowflake. Ele vive em um schema como qualquer outro objeto e guarda o histórico de deployments.

Aqui está a estrutura do meu repositório inicial:

platform/
dcm/
manifest.yml
sources/definitions/
roles.sql
databases.sql
warehouses.sql
grants.sql
migrations/
000_bootstrap.sql
001_ci_deployer.sql

Um arquivo por tipo de objeto é uma convenção minha, não uma regra do DCM. O Snowflake lê tudo dentro de sources/, então organize do jeito que fizer sentido para você.

DEFINE, não CREATE

Essa é toda a mudança de mentalidade. Com um script de migração, você escreve instruções:

CREATE ROLE IF NOT EXISTS DEV_LOADER;
ALTER ROLE DEV_LOADER SET COMMENT = 'Owns DEV_RAW';

Com o DCM, você escreve o estado final:

DEFINE ROLE DEV_LOADER
COMMENT = 'Owns DEV_RAW. Used by ingestion tools.';

Se a role não existe, o DCM a cria. Se ela existe com um comentário diferente, o DCM a altera. Se já estiver igual, o DCM não faz nada. Você nunca mais escreve IF NOT EXISTS, e nunca mais escreve um ALTER para corrigir algo que você mesmo escreveu na semana passada.

Grants funcionam do mesmo jeito. Você não faz DEFINE de um grant, apenas o declara:

DEFINE DATABASE DEV_RAW
COMMENT = 'Landing zone. Schemas and tables are created by ingestion tools.';
GRANT OWNERSHIP ON DATABASE DEV_RAW TO ROLE DEV_LOADER;

Planeje, depois faça o deploy

Toda mudança passa por duas etapas. Com o Snowflake CLI:

snow dcm plan --from platform/dcm --target DEV \
--connection my_account --role PLATFORM_DEPLOYER

O plano compara seus arquivos com o que está na conta e diz exatamente o que seria criado, alterado e removido. Ele não muda nada.

[screenshot: plan output for adding a database]

Gostou do resultado? Faça o deploy:

snow dcm deploy --from platform/dcm --target DEV --alias add_marketing_db \
--connection my_account --role PLATFORM_DEPLOYER

O --alias é opcional, mas use, por favor. Sem ele, seu histórico de deployments vira uma lista de nomes gerados automaticamente que ninguém consegue ler. Com ele, o snow dcm list-deployments fica parecendo um changelog: add_marketing_db, analyst_read_on_analytics, drop_legacy_loader.

Você também pode fazer tudo isso em SQL (EXECUTE DCM PROJECT ... PLAN), e o Snowsight Workspaces tem uma interface para isso. Eu vivo no terminal, então o CLI é o que você vai ver aqui.

O DCM remove o que você deixa de definir

Essa é a parte que pode te pegar de surpresa.

Se você remover um DEFINE que já foi implantado, o próximo deploy remove aquele objeto.

Esse é o comportamento correto para uma ferramenta declarativa. Os arquivos são a conta. Mas isso significa que dois hábitos são inegociáveis:

  1. Sempre planeje antes do deploy. Um DROP inesperado no plano significa que uma definição sumiu, não que o DCM está sendo prestativo.
  2. Leia o plano no seu PR. Falo mais sobre como automatizo isso abaixo.

Mais um detalhe: a documentação do Snowflake é transparente ao dizer que um deploy com falha pode deixar uma execução parcial. Não é uma grande transação única. Corrija a definição e rode plan → deploy de novo, em vez de consertar as coisas na mão.

DEV, QA e PROD a partir de um único conjunto de arquivos

Ninguém quer três cópias do roles.sql. O manifest.yml define targets, e cada target aponta para seu próprio objeto de projeto e passa suas próprias variáveis de template:

manifest_version: 2
type: DCM_PROJECT
default_target: DEV
targets:
DEV:
account_identifier: ABCDEFG-XY12345
project_name: PLATFORM.DCM.DEV_PLATFORM_DCM
project_owner: PLATFORM_DEPLOYER
templating_config: DEV
# QA and PROD look the same
templating:
configurations:
DEV:

Expandir código

Depois, cada definição usa {{ env }} como prefixo:

DEFINE DATABASE {{ env }}_RAW
COMMENT = 'Landing zone. Schemas and tables are created by ingestion tools.';
DEFINE DATABASE {{ env }}_ANALYTICS
COMMENT = 'Modelled data. Schemas and tables are created by transformation tools.';
GRANT OWNERSHIP ON DATABASE {{ env }}_RAW TO ROLE {{ env }}_LOADER;
GRANT OWNERSHIP ON DATABASE {{ env }}_ANALYTICS TO ROLE {{ env }}_TRANSFORMER;

Faça o deploy com --target DEV e você obtém DEV_RAW. Faça o deploy com --target PROD e você obtém PROD_RAW. Os mesmos arquivos.

A parte complicada: objetos de nível de conta

Alguns objetos não pertencem a um ambiente específico. No meu template, os três ambientes compartilham uma conta, um warehouse e um conjunto de roles "master" como LOADER, que herdam DEV_LOADER, QA_LOADER e PROD_LOADER.

Se eu definisse COMPUTE_XS sem nenhuma condição, os três targets reivindicariam o objeto, e o deploy de cada target brigaria por ele. Então os objetos de nível de conta são definidos em exatamente um target, com um if do Jinja e um loop:

{% if env == 'PROD' %}
{% set envs = ['DEV', 'QA', 'PROD'] %}
DEFINE WAREHOUSE COMPUTE_XS
WAREHOUSE_TYPE = 'ADAPTIVE'
MAX_QUERY_PERFORMANCE_LEVEL = XSMALL
QUERY_THROUGHPUT_MULTIPLIER = 2
COMMENT = 'Sole compute warehouse for loads and transforms.';
{% for e in envs %}
GRANT USAGE ON WAREHOUSE COMPUTE_XS TO ROLE {{ e }}_LOADER;
GRANT USAGE ON WAREHOUSE COMPUTE_XS TO ROLE {{ e }}_TRANSFORMER;
GRANT USAGE ON WAREHOUSE COMPUTE_XS TO ROLE {{ e }}_ANALYST;
{% endfor %}
{% endif %}

O detalhe: aquele bloco de PROD concede grants para DEV_LOADER e QA_LOADER, então DEV e QA precisam fazer deploy antes de PROD. A ordem de deploy passa a fazer parte do seu processo — e do seu CI.

Se você colocar cada ambiente em sua própria conta, isso desaparece, mas aí cada conta precisa da sua própria cópia dos objetos de nível de conta. Escolha o seu trade-off.

Este é o modelo de roles que o template entrega no final:

Além disso, a role de Deployer recebe diretamente as roles específicas de cada ambiente:

Sua ferramenta de ingestão se conecta como {env}_LOADER. O dbt se conecta como {env}_TRANSFORMER. Os analistas recebem {env}_ANALYST, que pode ler ANALYTICS e não enxerga RAW de forma alguma.

Se você usa dbt, deixe o dbt ser o dono das tabelas e schemas no banco Analytics

O DCM suporta uma longa lista de tipos de objeto: bancos de dados, schemas, tabelas, views, dynamic tables, tasks, stages, functions, procedures, masking policies, tags e mais.

Então por que o meu template só o usa para roles, bancos de dados, warehouses e grants?

Porque, se você roda dbt, o dbt já gerencia suas tabelas e views, com linhagem, testes e documentação. Coloque a mesma tabela em uma definição do DCM e agora você tem duas ferramentas que acham que são donas dela. Uma delas vai acabar derrubando o que a outra construiu.

Por isso, eu traço uma linha bem clara:

Nenhum objeto tem dois donos. Se você não usa dbt, ou se tem objetos que nenhuma ferramenta de transformação gerencia (stages, file formats, network rules), o DCM é um ótimo lugar para eles. A regra é "um dono por objeto", não "o DCM só cuida de roles".

O ovo e a galinha: bootstrap

O DCM precisa de uma role para fazer o deploy e de um objeto de projeto onde fazer o deploy. Algo tem que criar essas coisas, e esse algo precisa de ACCOUNTADMIN.

Minha regra para o repositório inteiro:

O CI/CD nunca deve precisar de ACCOUNTADMIN.

Então tudo que realmente precisa dele vai para um arquivo SQL numerado que uma pessoa executa exatamente uma vez:

USE ROLE ACCOUNTADMIN;
ALTER ACCOUNT SET FEATURE_RBAC_INHERITED_GRANTS = 'ENABLED';
CREATE ROLE IF NOT EXISTS PLATFORM_DEPLOYER
COMMENT = 'CI/CD deployment role for DCM.';
GRANT CREATE ROLE ON ACCOUNT TO ROLE PLATFORM_DEPLOYER;
GRANT CREATE DATABASE ON ACCOUNT TO ROLE PLATFORM_DEPLOYER;
GRANT CREATE WAREHOUSE ON ACCOUNT TO ROLE PLATFORM_DEPLOYER;
GRANT MANAGE GRANTS ON ACCOUNT TO ROLE PLATFORM_DEPLOYER;
GRANT ROLE PLATFORM_DEPLOYER TO ROLE SYSADMIN;
CREATE DATABASE IF NOT EXISTS PLATFORM;
CREATE SCHEMA IF NOT EXISTS PLATFORM.DCM;

Expandir código

(Levemente resumido. O arquivo completo está no repositório.)

Depois disso, o PLATFORM_DEPLOYER faz tudo. E aqui vai um hábito que eu adoro: passe --role PLATFORM_DEPLOYER localmente também. Se uma mudança secretamente precisar de mais privilégios, ela falha no seu laptop, e não no CI numa sexta-feira à tarde.

Armadilhas em que eu caí para você não precisar cair

Inherited grants no DCM estão marcados como preview. A lista de objetos suportados pelo DCM do Snowflake sinaliza inherited grants e MANAGE GRANTS em nível de contêiner como recursos em preview. Funcionaram bem para mim, mas confira a documentação antes de apostar o acesso de produção neles.

Não deixe o deployer trancado do lado de fora. Quando o DCM entrega a propriedade de DEV_RAW para DEV_LOADER, o PLATFORM_DEPLOYER deixa de ser o dono. No próximo deploy, o DCM não consegue gerenciar o que não alcança. A correção é uma linha por role:

GRANT ROLE {{ env }}_LOADER TO ROLE PLATFORM_DEPLOYER;
GRANT ROLE {{ env }}_TRANSFORMER TO ROLE PLATFORM_DEPLOYER;

O account_identifier no manifest não expande variáveis de ambiente. Escreva o identificador diretamente. É isso que permite ao DCM te avisar quando você está prestes a fazer deploy na conta errada — o que, se você é consultor fazendo malabarismo com contas de vários clientes, é um recurso que você quer ter.

Não use templates para secrets. O Snowflake diz com todas as letras: variáveis de template não foram feitas para credenciais.

CI/CD com GitHub Actions

Aqui é onde tudo se encaixa. Dois workflows:

Em um pull request:

  1. Plan e deploy em QA.
  2. Plan de PROD e publicação do plano como comentário no PR.

No merge para main:

  1. Plan e deploy de DEV, depois QA, depois PROD, nessa ordem.

O comentário no PR é a minha parte favorita. Os revisores não precisam confiar que uma mudança é segura. Eles veem exatamente o que vai acontecer em produção, incluindo qualquer DROP, antes de clicar em merge. E o job edita o comentário anterior em vez de adicionar um novo, então um PR com dez pushes continua com um único comentário de plano.

[screenshot: PROD plan posted as a PR comment]

O CI se autentica como um usuário TYPE = SERVICE com autenticação por par de chaves. Esse usuário tem a role PLATFORM_DEPLOYER e nada mais. Todo deploy recebe um alias com o commit (main_9fd3c1a), então qualquer deployment no Snowflake pode ser rastreado direto até um merge.

DCM Projects no Snowsight

O Snowflake Workspaces oferece uma interface muito legal para gerenciar, planejar e fazer deploy de DCM Projects. Sem entrar em muitos detalhes, vamos só dar uma olhada em algumas capturas de tela.

Ao executar um plan, uma aba se abre explicando o plano:

Você pode trocar de ambiente usando o seletor:

Quando estiver pronto para o deploy, basta clicar no menu suspenso de Plan e selecionar deploy:

Quando terminar, uma notificação aparece na parte superior central da tela avisando que o deploy foi concluído.

A aba Output mostra a saída do CLI de todas as suas execuções:

Se tivéssemos um DAG de Dynamic Tables ou Tasks, eles apareceriam na aba de linhagem, mas este projeto não contém nenhum.

Executando o DCM localmente com o CLI

Pessoalmente, eu faço todo o meu trabalho no Visual Studio Code. Aqui vai um resumo rápido de como é o deployment no VS Code. dcm é um subcomando do comando snow do CLI. Então, se você tem o CLI snow instalado, já tem o DCM! Aqui estou mostrando snow dcm plan e snow dcm deploy em uma única captura de tela.

Agora posso iterar nos arquivos localmente, fazer deploy no meu ambiente de dev, abrir um PR que vai planejar e fazer deploy em QA e rodar o plano contra prod. Um workflow muito amigável para quem desenvolve!

DCM vs. as alternativas

Se você gerencia AWS, Snowflake e seu DNS em um único repositório Terraform, o Terraform ainda faz sentido. Se a infraestrutura do seu time é basicamente Snowflake e o time já fala SQL, o DCM é o caminho de menor atrito que encontrei.

Experimente você mesmo

Coloquei tudo isso em um repositório inicial: https://github.com/jeff-skoldberg-gmds/snowflake-dcm-starter.

Ele inclui as definições, as migrações de bootstrap, os dois workflows de GitHub Actions e skills do Claude Code. Rode /project-setup e ele te guia pela conta, pelo manifest, pelo bootstrap, pelo primeiro deploy e pelo CI, um passo verificado de cada vez.

Ele também tem pastas ingest/ e dbt/ vazias, porque o DCM é só a base. Você deve construir um monorepo para a sua plataforma Snowflake que inclua a carga e a transformação dos dados. As roles e os bancos de dados estão esperando pelos seus pipelines.

Agora vá testar e me conte se infraestrutura como código no Snowflake ainda parece uma tarefa chata!