SELECTSELECT

SELECT

Snowflake DCM Projects : l'infrastructure as code, en pur SQL

Cette page est également disponible en English, Deutsch, Español, Italiano, 日本語 et Português.

By SELECTNov 12, 2022 min read

TL;DR

  • Les DCM Projects sont la solution native et déclarative de Snowflake pour gérer vos objets sous forme de code. Vous écrivez DEFINE <object> au lieu de CREATE <object> (par exemple DEFINE DATABASE), et Snowflake détermine ce qui doit changer.
  • Le cycle : modifier les définitions, planifier, puis déployer. Le plan est un diff en lecture seule. Le déploiement l'applique.
  • DCM supprime ce que vous cessez de définir. Une ligne supprimée, une base de données en moins. Lisez le plan.
  • Un manifest.yml et un peu de Jinja suffisent pour obtenir DEV, QA et PROD à partir d'un seul jeu de fichiers.
  • DCM peut gérer les tables et les vues, mais à mon avis il ne devrait pas dès lors que vous utilisez déjà dbt. Laissez DCM gérer la plateforme (rôles, bases de données, warehouses, grants) et dbt gérer ce qui se trouve dans les bases.
  • Le design des rôles et des bases de données reprend le guide de configuration Snowflake de dbt Labs, suivi par des centaines de projets dbt. Le repo y apporte quelques améliorations et le transforme en code.
  • Voici le repo GitHub sur lequel s'appuie cet article, avec une CI/CD via GitHub Actions qui ne touche jamais à ACCOUNTADMIN.

Comment en est-on arrivé là

Si vous utilisez Snowflake depuis un moment, vous avez probablement géré l'infrastructure de votre compte de l'une de ces trois façons :

  1. Le click-ops. Quelqu'un avec ACCOUNTADMIN crée un objet dans Snowsight un mardi. Personne ne se souvient pourquoi. Trois ans plus tard, personne n'ose le supprimer.
  2. Les scripts de migration : V1__create_roles.sql, V2__grant_things.sql, V47__fix_the_grant_from_V2.sql. Des outils comme schemachange les exécutent dans l'ordre. Ça fonctionne, mais l'état actuel de votre compte est la somme de 47 fichiers, et bon courage pour lire ça.
  3. Terraform. Déclaratif et puissant. Mais il apporte aussi HCL, un provider à maintenir à jour, et un fichier d'état à stocker, verrouiller et, de temps en temps, opérer à cœur ouvert. Pour beaucoup d'équipes data, c'est toute une discipline à apprendre juste pour créer un warehouse.

Les DCM Projects sont la réponse de Snowflake. Vous obtenez le modèle déclaratif de Terraform, mais le langage est le SQL, et l'état vit dans Snowflake.

Le modèle de départ : les grants que tout le monde a copiés

Avant d'entrer dans DCM, rendons à César ce qui est à César. Si vous avez configuré Snowflake pour dbt, il y a fort à parier que vous ayez lu l'article de Claire Carroll Setting up Snowflake — the exact grant statements we run sur le Discourse de dbt. Des centaines de projets dbt ont été mis en place en suivant ce schéma. Je l'ai moi-même suivi un nombre incalculable de fois.

Le design est simple, et c'est pour cela qu'il fonctionne :

  • Une base raw pour les données entrantes, et une base analytics pour les données modélisées.
  • Un rôle loader qui écrit dans raw, un rôle transformer qui lit raw et construit analytics, et un rôle reporter qui lit analytics et rien d'autre.
  • Des future grants, pour que les nouveaux schémas et tables soient lisibles sans que personne ne lève le petit doigt.

Le problème n'a jamais été le design. Le problème, c'est qu'il s'agit d'une liste d'instructions que l'on colle une fois dans un worksheet. Six mois plus tard, personne ne peut dire si le compte y correspond encore, et l'extension environnement de dev en bas de l'article est laissée en exercice au lecteur.

Le repo de démarrage conserve donc la structure de Claire et change quelques éléments :

Pourquoi les inherited grants remplacent les future grants

Cette ligne sur les grants est le changement le plus important, alors creusons un peu.

Les future grants ont longtemps été le bon outil ! C'est ce qui rend le setup de Claire si peu coûteux à maintenir. Mais ils posent trois problèmes, et quiconque administre Snowflake depuis quelques années en a rencontré au moins un :

  1. Ils ne couvrent que le futur. Un future grant ne fait rien pour les tables qui existent déjà, donc chaque setup l'associe à une instruction grant select on all tables. Cela fait deux instructions par privilège, et deux choses à retenir.
  2. Les future grants au niveau schéma écrasent ceux au niveau base. Si quelqu'un ajoute un future grant sur un schéma, Snowflake ignore les future grants définis au niveau de la base pour ce schéma. Votre rôle reporter cesse discrètement de recevoir les nouvelles tables à cet endroit, sans la moindre erreur.
  3. C'est une copie ponctuelle, pas une règle. Un future grant est appliqué à chaque objet au moment de sa création. Révoquez ou modifiez le future grant plus tard : tous les objets qu'il a déjà touchés conservent l'ancien privilège. Le grant que vous voyez ne décrit plus les accès dont les gens disposent réellement.

Un inherited grant, c'est une seule instruction sur un conteneur (le compte, une base de données ou un schéma), et elle s'applique à chaque objet correspondant qu'il contient, existant ou futur :

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

Voilà le remplaçant, à lui seul, du future grant, du grant on all et de la version par schéma de chacun. Quand vous voulez savoir pourquoi quelqu'un peut lire une table, SHOW GRANTS dispose des colonnes IS_INHERITED et INHERITED_FROM, qui pointent vers le grant responsable.

C'est aussi un choix naturel pour DCM. Un outil déclaratif veut une ligne qui énonce la règle. Les analystes peuvent tout lire dans ANALYTICS tient désormais en une ligne, pas six.

Quelques compromis à connaître :

  • Pas d'exceptions. Vous ne pouvez pas révoquer le privilège sur une table pour l'exclure. La documentation Snowflake prévient que la révocation semble réussir alors que le rôle conserve l'accès via l'inherited grant. Si une table nécessite des accès différents, sa place est dans un autre schéma ou une autre base.
  • Déplacer ou cloner un objet dans un conteneur change qui peut le voir, sans la moindre instruction GRANT. C'est le but, mais soyez vigilant avec les clones.
  • OWNERSHIP ne peut pas être hérité. La propriété reste un grant classique.
  • Un paramètre de compte est requis, FEATURE_RBAC_INHERITED_GRANTS, que seul ACCOUNTADMIN peut activer. On y revient dans la partie bootstrap ci-dessous.

Qu'est-ce qu'un DCM Project dans Snowflake ?

Deux choses :

  1. Un dossier de fichiers. Un manifest.yml et des fichiers .sql remplis d'instructions DEFINE.
  2. Un objet DCM project dans Snowflake. Il vit dans un schéma comme n'importe quel autre objet et conserve l'historique des déploiements.

Voici l'arborescence de mon repo de démarrage :

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

Un fichier par type d'objet, c'est ma convention, pas une règle de DCM. Snowflake lit tout ce qui se trouve sous sources/, alors organisez le tout comme bon vous semble.

DEFINE, pas CREATE

Tout le changement de paradigme est là. Avec un script de migration, vous écrivez des instructions :

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

Avec DCM, vous écrivez l'état final :

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

Si le rôle n'existe pas, DCM le crée. S'il existe avec un commentaire différent, DCM le modifie. S'il correspond déjà, DCM ne fait rien. Vous n'écrirez plus jamais de IF NOT EXISTS, ni d'ALTER pour corriger ce que vous avez écrit la semaine dernière.

Les grants fonctionnent de la même manière. Vous ne faites pas de DEFINE sur un grant, vous l'énoncez, tout simplement :

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;

Planifier, puis déployer

Chaque changement passe par deux étapes. Avec la CLI Snowflake :

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

Le plan compare vos fichiers à ce qui se trouve dans le compte et vous indique précisément ce qu'il créerait, modifierait et supprimerait. Il ne change rien.

[capture d'écran : sortie du plan pour l'ajout d'une base de données]

Satisfait du résultat ? Déployez :

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

Le --alias est optionnel, mais utilisez-le, vraiment. Sans alias, votre historique de déploiements est une liste de noms auto-générés que personne ne peut lire. Avec, snow dcm list-deployments se lit comme un changelog : add_marketing_db, analyst_read_on_analytics, drop_legacy_loader.

Tout cela est aussi faisable en SQL (EXECUTE DCM PROJECT ... PLAN), et les Workspaces de Snowsight proposent une interface dédiée. Je passe ma vie dans le terminal, donc c'est la CLI que vous verrez ici.

DCM supprime ce que vous cessez de définir

C'est la partie qui peut vous jouer des tours.

Si vous retirez un DEFINE déjà déployé, le prochain déploiement supprime l'objet concerné.

C'est le comportement attendu d'un outil déclaratif. Les fichiers sont le compte. Mais cela rend deux habitudes non négociables :

  1. Toujours planifier avant de déployer. Un DROP inattendu dans le plan signifie qu'une définition a disparu, pas que DCM vous rend service.
  2. Lire le plan dans votre PR. J'explique plus bas comment j'automatise cela.

Une dernière chose : la documentation Snowflake est claire sur le fait qu'un déploiement en échec peut vous laisser avec une exécution partielle. Ce n'est pas une seule grande transaction. Corrigez la définition et relancez plan → deploy, plutôt que de rafistoler à la main.

DEV, QA et PROD à partir d'un seul jeu de fichiers

Personne ne veut trois copies de roles.sql. Le manifest.yml définit des targets, et chaque target pointe vers son propre objet projet et transmet ses propres variables de templating :

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:

Afficher le code

Ensuite, chaque définition utilise {{ env }} comme préfixe :

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;

Déployez avec --target DEV et vous obtenez DEV_RAW. Déployez avec --target PROD et vous obtenez PROD_RAW. Les mêmes fichiers.

Le point délicat : les objets au niveau du compte

Certains objets n'appartiennent à aucun environnement. Dans mon template, les trois environnements partagent un seul compte, un seul warehouse et un ensemble de rôles maîtres comme LOADER, qui héritent de DEV_LOADER, QA_LOADER et PROD_LOADER.

Si je définissais COMPUTE_XS sans condition, les trois targets le revendiqueraient, et chaque déploiement se le disputerait. Les objets au niveau du compte sont donc définis dans exactement un target, avec un if Jinja et une boucle :

{% 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 %}

Le piège : ce bloc PROD accorde des privilèges à DEV_LOADER et QA_LOADER, donc DEV et QA doivent être déployés avant PROD. L'ordre de déploiement devient une partie de votre process, et de votre CI.

Si vous placez chaque environnement dans son propre compte, le problème disparaît, mais chaque compte a alors besoin de sa propre copie des objets au niveau du compte. À vous de choisir votre compromis.

Voici le modèle de rôles auquel aboutit le template :

Et le rôle Deployer reçoit directement les rôles propres à chaque environnement :

Votre outil d'ingestion se connecte en tant que {env}_LOADER. dbt se connecte en tant que {env}_TRANSFORMER. Les analystes reçoivent {env}_ANALYST, qui peut lire ANALYTICS et ne voit pas du tout RAW.

Si vous utilisez dbt, laissez-lui la propriété des tables et des schémas de la base Analytics

DCM prend en charge une longue liste de types d'objets : bases de données, schémas, tables, vues, dynamic tables, tasks, stages, fonctions, procédures, masking policies, tags, et plus encore.

Alors pourquoi mon template ne l'utilise-t-il que pour les rôles, les bases de données, les warehouses et les grants ?

Parce que si vous utilisez dbt, dbt gère déjà vos tables et vos vues, avec le lineage, les tests et la documentation. Mettez la même table dans une définition DCM et vous avez désormais deux outils qui pensent chacun en être propriétaire. L'un finira par supprimer ce que l'autre a construit.

Je trace donc une ligne nette :

Aucun objet n'a deux propriétaires. Si vous n'utilisez pas dbt, ou si vous avez des objets qu'aucun outil de transformation ne gère (stages, file formats, network rules), DCM est un excellent endroit pour les héberger. La règle, c'est un propriétaire par objet, pas DCM ne gère que les rôles.

La poule et l'œuf : le bootstrap

DCM a besoin d'un rôle pour déployer et d'un objet projet dans lequel déployer. Quelque chose doit les créer, et ce quelque chose a besoin d'ACCOUNTADMIN.

Ma règle pour l'ensemble du repo :

La CI/CD ne doit jamais avoir besoin d'ACCOUNTADMIN.

Tout ce qui en a besoin va donc dans un fichier SQL numéroté qu'un humain exécute exactement une fois :

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;

Afficher le code

(Légèrement raccourci. Le fichier complet est dans le repo.)

Ensuite, PLATFORM_DEPLOYER fait tout. Et voici une habitude que j'adore : passez --role PLATFORM_DEPLOYER en local aussi. Si un changement a secrètement besoin de privilèges supplémentaires, il échoue sur votre laptop plutôt qu'en CI un vendredi après-midi.

Les pièges dans lesquels je suis tombé pour vous les épargner

Les inherited grants dans DCM sont marqués comme preview. La liste des objets DCM pris en charge par Snowflake signale les inherited grants et le MANAGE GRANTS au niveau conteneur comme des fonctionnalités en preview. Ils ont bien fonctionné pour moi, mais vérifiez la documentation avant de miser vos accès de production dessus.

Ne mettez pas le deployer à la porte. Quand DCM transfère la propriété de DEV_RAW à DEV_LOADER, PLATFORM_DEPLOYER n'en est plus propriétaire. Au déploiement suivant, DCM ne peut pas gérer ce qu'il ne peut pas atteindre. La correction tient en une ligne par rôle :

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

account_identifier dans le manifest n'interprète pas les variables d'environnement. Écrivez l'identifiant en dur. C'est ce qui permet à DCM de vous avertir quand vous êtes sur le point de déployer sur le mauvais compte — et si vous êtes consultant et jonglez entre les comptes clients, c'est une fonctionnalité dont vous ne voudrez plus vous passer.

Pas de secrets dans les templates. Snowflake le dit sans détour : les variables de templating ne sont pas faites pour les identifiants.

CI/CD avec GitHub Actions

C'est ici que tout se rejoint. Deux workflows :

Sur une pull request :

  1. Plan et déploiement vers QA.
  2. Plan de PROD et publication du plan en commentaire de la PR.

Au merge sur main :

  1. Plan et déploiement de DEV, puis QA, puis PROD, dans cet ordre.

Le commentaire de PR est ma partie préférée. Les reviewers n'ont pas à croire sur parole qu'un changement est sûr. Ils voient exactement ce qui arrivera en production, y compris le moindre DROP, avant de cliquer sur merge. Et le job modifie son commentaire précédent au lieu d'en ajouter un nouveau : une PR avec dix pushes n'a toujours qu'un seul commentaire de plan.

[capture d'écran : plan PROD publié en commentaire de PR]

La CI s'authentifie en tant qu'utilisateur TYPE = SERVICE avec une authentification par paire de clés. Cet utilisateur détient PLATFORM_DEPLOYER et rien d'autre. Chaque déploiement est aliasé avec le commit (main_9fd3c1a), si bien que tout déploiement dans Snowflake remonte directement à un merge.

Les DCM Projects dans Snowsight

Les Workspaces de Snowflake offrent une interface vraiment réussie pour gérer, planifier et déployer les DCM projects. Sans entrer dans les détails, passons simplement en revue quelques captures d'écran.

Lancer un plan ouvre un onglet qui l'explique :

Vous pouvez changer d'environnement via le sélecteur :

Quand vous êtes prêt à déployer, cliquez simplement sur le menu déroulant Plan et sélectionnez deploy :

Une fois l'opération terminée, une notification apparaît en haut au centre de l'écran pour vous indiquer que le déploiement est effectué.

L'onglet Output affiche la sortie CLI de toutes vos exécutions :

Si nous avions un DAG de Dynamic Tables ou de Tasks, il apparaîtrait dans l'onglet lineage, mais ce projet n'en contient pas.

Exécuter DCM en local avec la CLI

Personnellement, je fais tout mon travail dans Visual Studio Code. Voici un aperçu rapide de ce à quoi ressemble un déploiement dans VS Code. dcm est une sous-commande de la commande CLI snow. Donc tant que la CLI snow est installée, vous avez déjà DCM ! Ici, je montre snow dcm plan et snow dcm deploy dans une seule capture d'écran.

Je peux désormais itérer sur les fichiers en local, déployer dans mon environnement de dev, ouvrir une PR qui planifiera et déploiera QA, puis exécutera le plan sur la prod. Un workflow très agréable pour les développeurs !

DCM face aux alternatives

Si vous gérez AWS, Snowflake et votre DNS dans un seul repo Terraform, Terraform reste pertinent. Si l'infrastructure de votre équipe se résume essentiellement à Snowflake et que votre équipe parle déjà SQL, DCM est la voie la moins contraignante que j'aie trouvée.

Essayez par vous-même

J'ai rassemblé tout cela dans un repo de démarrage : https://github.com/jeff-skoldberg-gmds/snowflake-dcm-starter.

Il contient les définitions, les migrations de bootstrap, les deux workflows GitHub Actions et des skills Claude Code. Lancez /project-setup et il vous guide à travers le compte, le manifest, le bootstrap, le premier déploiement et la CI, une étape validée à la fois.

Il contient aussi des dossiers ingest/ et dbt/ vides, parce que DCM n'est que la fondation. Construisez un mono-repo pour votre plateforme Snowflake qui inclut le chargement et la transformation des données. Les rôles et les bases de données n'attendent plus que vos pipelines.

Maintenant, essayez, et dites-moi si l'infrastructure as code sur Snowflake vous semble toujours une corvée !x`