更新日:2025年9月30日

23分で読めます

GitLab Duo Agent PlatformがDataOpsを変革する方法

本ブログでは、dbtモデル作成方法を詳しく解説し、何時間もかかっていた手動コーディングを、わずか数分の自動生成に変える方法をご紹介します。

dbtモデルの手動作成は、データエンジニアにとって何時間も取られる単調な作業です。特に大きなビジネス変換がない場合、エンジニアのデータ業務の中でも最もやりがいを感じにくい部分と言えるでしょう。

しかし、このプロセス全体を自動化できるとしたらどうでしょうか?本ブログでは、GitLab Duo Agent Platformを使用して、適切な構造、テスト、ドキュメントを含む包括的なdbtモデルをわずか数分で生成する方法を詳しく解説します。

何を構築するのか

マーケティングチームは、広告投資を効果的に管理・最適化したいと考えています。広告プラットフォームの一つがRedditであるため、Reddit Ads APIからエンタープライズデータプラットフォームSnowflakeにデータを抽出しています。GitLabでは、3つのストレージレイヤーがあります:

  1. rawレイヤー - 外部ソースからの未処理データの最初の着陸地点。ビジネス用途に準備されていない

  2. prepレイヤー - ソースモデルを含む最初の変換レイヤー。まだ一般的なビジネス用途には準備されていない

  3. prodレイヤー - ビジネス用途とTableauレポート用に準備された最終変換データ

ストレージレイヤーのチャート

この実践ガイドでは、データは既に抽出ツールFivetranによってrawレイヤーに配置されており、prepレイヤーからprodレイヤーまでデータを処理するdbtモデルを生成します。

一行のdbtコードを自分で書くことなく、このガイドを最後まで進めると以下が完成します:

  • prepレイヤーのソースモデル

  • prodレイヤーのワークスペースモデル

  • Reddit Adsデータセットのすべての13テーブル(112列を含む)の完全なdbt構成

  • 結果を検証するテストクエリ

プロセス全体は10分以内で完了します。手動では通常数時間かかる作業です。以下の手順に従ってください:

1. データ構造の準備

GitLab Duoがモデルを生成する前に、完全なテーブル構造を理解する必要があります。そこで重要になるのが、Snowflakeの情報スキーマに対してクエリを実行することです。現在、GitLab DuoをModel Context Protocol(MCP)経由でSnowflakeインスタンスに接続する方法を検討中です:


SELECT 
    table_name,
    column_name,
    data_type,
    is_nullable,
    CASE 
        WHEN is_nullable = 'NO' THEN 'PRIMARY_KEY'
        ELSE NULL 
    END as key_type
FROM raw.information_schema.columns


WHERE table_schema = 'REDDIT_ADS'


ORDER BY table_name, ordinal_position;

このクエリは以下を取得します:

  • すべてのテーブル名と列名

  • 適切なモデル構造のためのデータ型

  • NULL制約

  • 主キーの識別(このデータセットでは非NULL列)

ポイント: Reddit Adsデータセットでは、すべての非NULL列が主キーとして機能するパターンになっています。実際にad_groupなどのテーブルを確認したところ、このテーブルには2つの非NULL列(account_idid)があり、両方とも主キーとしてマークされています。このクエリを実行すると112行のメタデータが返されたため、それらをCSVファイルとしてエクスポートしてモデル生成に使用しました。この手動ステップは現在は問題なく機能していますが、MCPを介してデータプラットフォームとGitLab Duoの直接統合を調査して、このプロセスの完全自動化を目指して取り組んでいます。

2. GitLab Duoのセットアップ

GitLab Duoと対話する方法は2つあります:

  1. Web UIチャット機能

  2. Visual Studio Codeプラグイン

dbtモデルをローカルで実行してテストできるため、VS Codeプラグインを選択しました。

3. 「効果的な」プロンプトの入力

すべてのdbtコードを生成するために使用した実際のプロンプトは以下の通りです:


Create dbt models for all the tables in the file structure.csv.(structure.csvファイル内のすべてのテーブルに対してdbtモデルを作成してください)



I want to have the source models created, with a filter that dedupes the data based on the primary key. Create these in a new folder reddit_ads.(主キーに基づいてデータの重複を除去するフィルターを使用してソースモデルを作成したいです。これらを新しいフォルダーreddit_adsに作成してください。)


I want to have workspace models created and store these in the workspace_marketing schema.(ワークスペースモデルを作成し、これらをworkspace_marketingスキーマに保存してください。)



Take this MR as example: [I've referenced to previous source implementation]. Here is the same done for Source A, but now it needs to be done for Reddit Ads. 


(このMRを例として参考にしてください: [以前のソース実装を参照]。これはSource Aで実施した内容ですが、同様の処理をReddit Adsに対して行う必要があります。)



Please check the dbt style guide when creating the code: https://handbook.gitlab.com/handbook/enterprise-data/platform/dbt-guide/(コードを作成する際は、dbtスタイルガイドをチェックしてください)

このプロンプトが効果的だった理由:

  • ソースモデルとワークスペースモデル両方の明確な仕様

  • 以前の類似マージリクエストからの参考例

  • コードの品質と一貫性を確保するためのスタイルガイド参照

  • 適切な構成管理のための特定のスキーマ指定

4. GitLab Duoのプロセス

プロンプトを送信した後、GitLab Duoが作業を開始しました。生成プロセス全体は数分かかり、その間にGitLab Duoは以下を行いました:

  1. CSV入力ファイルを読み取り分析

  2. メタデータからテーブル構造を調査

  3. コーディング標準についてdbtスタイルガイドを参照

  4. 適切な構造化のために類似のマージリクエストを考慮

  5. すべての13テーブルのソースモデルを生成

  6. すべての13テーブルのワークスペースモデルを作成

  7. dbtサポートファイルを生成

    • sources.yml構成
    • テストとドキュメントを含むschema.ymlファイル
    • スキーマ参照でdbt_project.ymlを更新

結果

出力は素晴らしいものでした:

  • 変更ファイル1個: dbt_project.yml(reddit_adsスキーマ構成を追加)

  • 新しいファイル29個:

    • dbtモデル26個(ソース13個 + ワークスペース13個)
    • YAMLファイル3個
  • 900行近くのコードが自動生成

  • 主キー列の一意制約を含む組み込みデータテスト

  • すべてのモデルと列の汎用説明

  • ソースモデルの適切な重複除去ロジック

  • GitLab dbtスタイルガイドに従ったクリーンで一貫したコード構造


transform/snowflake-dbt/


├── dbt_project.yml                                                    [MODIFIED](変更)


└── models/
    ├── sources/
    │   └── reddit_ads/
    │       ├── reddit_ads_ad_group_source.sql                        [NEW](新規)
    │       ├── reddit_ads_ad_source.sql                              [NEW]
    │       ├── reddit_ads_business_account_source.sql                [NEW]
    │       ├── reddit_ads_campaign_source.sql                        [NEW]
    │       ├── reddit_ads_custom_audience_history_source.sql         [NEW]
    │       ├── reddit_ads_geolocation_source.sql                     [NEW]
    │       ├── reddit_ads_interest_source.sql                        [NEW]
    │       ├── reddit_ads_targeting_community_source.sql             [NEW]
    │       ├── reddit_ads_targeting_custom_audience_source.sql       [NEW]
    │       ├── reddit_ads_targeting_device_source.sql                [NEW]
    │       ├── reddit_ads_targeting_geolocation_source.sql           [NEW]
    │       ├── reddit_ads_targeting_interest_source.sql              [NEW]
    │       ├── reddit_ads_time_zone_source.sql                       [NEW]
    │       ├── schema.yml                                            [NEW]
    │       └── sources.yml                                           [NEW]
    └── workspaces/
        └── workspace_marketing/
            └── reddit_ads/
                ├── schema.yml                                        [NEW]
                ├── wk_reddit_ads_ad.sql                              [NEW]
                ├── wk_reddit_ads_ad_group.sql                        [NEW]
                ├── wk_reddit_ads_business_account.sql                [NEW]
                ├── wk_reddit_ads_campaign.sql                        [NEW]
                ├── wk_reddit_ads_custom_audience_history.sql         [NEW]
                ├── wk_reddit_ads_geolocation.sql                     [NEW]
                ├── wk_reddit_ads_interest.sql                        [NEW]
                ├── wk_reddit_ads_targeting_community.sql             [NEW]
                ├── wk_reddit_ads_targeting_custom_audience.sql       [NEW]
                ├── wk_reddit_ads_targeting_device.sql                [NEW]
                ├── wk_reddit_ads_targeting_geolocation.sql           [NEW]
                ├── wk_reddit_ads_targeting_interest.sql              [NEW]
                └── wk_reddit_ads_time_zone.sql                       [NEW]

生成されたコードのサンプル

生成されたコード品質の例をご紹介します。time_zoneテーブルについて、GitLab Duoは以下を作成しました:

Prepレイヤーソースモデル


WITH source AS (
  SELECT *
  FROM {{ source('reddit_ads','time_zone') }}
  QUALIFY ROW_NUMBER() OVER (PARTITION BY id ORDER BY _fivetran_synced DESC) = 1
),



renamed AS (
  SELECT
    id::VARCHAR                               AS time_zone_id,
    code::VARCHAR                             AS time_zone_code,
    dst_offset::NUMBER                        AS time_zone_dst_offset,
    is_dst_active::BOOLEAN                    AS is_time_zone_dst_active,
    name::VARCHAR                             AS time_zone_name,
    offset::NUMBER                            AS time_zone_offset,
    _fivetran_synced::TIMESTAMP               AS fivetran_synced_at
  FROM source
)



SELECT * FROM renamed

Schema.yml


models:
  - name: reddit_ads_time_zone_source
    description: Time zone data from Reddit Ads system(Reddit Adsシステムからのタイムゾーンデータ)
    columns:
      - name: time_zone_id
        description: Time zone data from Reddit Ads system(タイムゾーンレコードの一意識別子)
        data_tests:
          - unique
          - not_null
      - name: time_zone_code
        description: Code for the time zone(タイムゾーンのコード)
      - name: time_zone_dst_offset
        description: Daylight saving time offset for the time zone(タイムゾーンの夏時間オフセット)
      - name: is_time_zone_dst_active
        description: Flag indicating if daylight saving time is active(夏時間がアクティブかどうかを示すフラグ)
      - name: time_zone_name
        description: Name of the time zone(タイムゾーンの名前)
      - name: time_zone_offset
        description: Offset for the time zone(タイムゾーンのオフセット)
      - name: fivetran_synced_at
        description: Timestamp when the record was last synced by Fivetran(レコードがFivetranによって最後に同期されたタイムスタンプ)

Source.yml


sources:
  - name: reddit_ads
    database: RAW
    schema: reddit_ads
    loaded_at_field: _fivetran_synced
    loader: fivetran
    description: Reddit Ads data

    quoting:
      database: true
      schema: false
      identifier: false

    tables:
      - name: time_zone

ワークスペースモデル


WITH source AS (
  SELECT *
  FROM {{ ref('reddit_ads_time_zone_source') }}
)



SELECT * FROM source

5. 品質検証

コードに問題がないことを確認し、MRにプッシュしてCIテストパイプラインを実行し、コードのテストと結果検証を行いました。GitLab Duoに検証クエリの作成を依頼しました:


Create a test query to test the row counts between the raw layer and the workspace layer. Keep in mind that we do deduplication, so we can compare both using distinct on the primary keys.(rawレイヤーとworkspaceレイヤー間の行数をテストするクエリを作成してください。重複除去を実施しているため、主キーに対してdistinctを使用して比較できます。)

AIが生成した包括的な検証クエリには、以下が含まれています:

  • rawレイヤーとワークスペースレイヤー間の行数を比較

  • 重複除去ロジックに対応

  • 全13テーブルをテスト

  • データ保持率を算出

生成されたSQLテストクエリ

-- Reddit Ads Row Count Validation Test(Reddit Ads行数検証テスト)


-- Compares distinct counts between RAW layer and WORKSPACE_MARKETING layer(RAWレイヤーとWORKSPACE_MARKETINGレイヤー間の一意カウントを比較)


-- Accounts for deduplication logic in source models(ソースモデルの重複除去ロジックを考慮)



WITH raw_counts AS (
  -- Single primary key tables(単一主キーテーブル)
  SELECT 'ad' AS table_name, COUNT(DISTINCT id) AS raw_count
  FROM RAW.REDDIT_ADS.AD
  
  UNION ALL
  
  SELECT 'business_account' AS table_name, COUNT(DISTINCT id) AS raw_count
  FROM RAW.REDDIT_ADS.BUSINESS_ACCOUNT
  
  UNION ALL
  
  SELECT 'campaign' AS table_name, COUNT(DISTINCT id) AS raw_count
  FROM RAW.REDDIT_ADS.CAMPAIGN
  
  UNION ALL
  
  SELECT 'custom_audience_history' AS table_name, COUNT(DISTINCT id) AS raw_count
  FROM RAW.REDDIT_ADS.CUSTOM_AUDIENCE_HISTORY
  
  UNION ALL
  
  SELECT 'geolocation' AS table_name, COUNT(DISTINCT id) AS raw_count
  FROM RAW.REDDIT_ADS.GEOLOCATION
  
  UNION ALL
  
  SELECT 'interest' AS table_name, COUNT(DISTINCT id) AS raw_count
  FROM RAW.REDDIT_ADS.INTEREST
  
  UNION ALL
  
  SELECT 'time_zone' AS table_name, COUNT(DISTINCT id) AS raw_count
  FROM RAW.REDDIT_ADS.TIME_ZONE
  
  -- Composite primary key tables(複合主キーテーブル)
  UNION ALL
  
  SELECT 'ad_group' AS table_name, COUNT(DISTINCT CONCAT(account_id, '|', id)) AS raw_count
  FROM RAW.REDDIT_ADS.AD_GROUP
  
  UNION ALL
  
  SELECT 'targeting_community' AS table_name, COUNT(DISTINCT CONCAT(ad_group_id, '|', community_id)) AS raw_count
  FROM RAW.REDDIT_ADS.TARGETING_COMMUNITY
  
  UNION ALL
  
  SELECT 'targeting_custom_audience' AS table_name, COUNT(DISTINCT CONCAT(ad_group_id, '|', custom_audience_id)) AS raw_count
  FROM RAW.REDDIT_ADS.TARGETING_CUSTOM_AUDIENCE
  
  UNION ALL
  
  SELECT 'targeting_device' AS table_name, COUNT(DISTINCT _fivetran_id) AS raw_count
  FROM RAW.REDDIT_ADS.TARGETING_DEVICE
  
  UNION ALL
  
  SELECT 'targeting_geolocation' AS table_name, COUNT(DISTINCT CONCAT(ad_group_id, '|', geolocation_id)) AS raw_count
  FROM RAW.REDDIT_ADS.TARGETING_GEOLOCATION
  
  UNION ALL
  
  SELECT 'targeting_interest' AS table_name, COUNT(DISTINCT CONCAT(ad_group_id, '|', interest_id)) AS raw_count
  FROM RAW.REDDIT_ADS.TARGETING_INTEREST
),



workspace_counts AS (
  -- Workspace layer counts using primary keys from schema.yml(schema.ymlの主キーを使用したワークスペースレイヤーカウント)
  SELECT 'ad' AS table_name, COUNT(DISTINCT ad_id) AS workspace_count
  FROM REDDIT_DBT_MODEL_GENERATION_PROD.WORKSPACE_MARKETING.WK_REDDIT_ADS_AD
  
  UNION ALL
  
  SELECT 'business_account' AS table_name, COUNT(DISTINCT business_account_id) AS workspace_count
  FROM REDDIT_DBT_MODEL_GENERATION_PROD.WORKSPACE_MARKETING.WK_REDDIT_ADS_BUSINESS_ACCOUNT
  
  UNION ALL
  
  SELECT 'campaign' AS table_name, COUNT(DISTINCT campaign_id) AS workspace_count
  FROM REDDIT_DBT_MODEL_GENERATION_PROD.WORKSPACE_MARKETING.WK_REDDIT_ADS_CAMPAIGN
  
  UNION ALL
  
  SELECT 'custom_audience_history' AS table_name, COUNT(DISTINCT custom_audience_id) AS workspace_count
  FROM REDDIT_DBT_MODEL_GENERATION_PROD.WORKSPACE_MARKETING.WK_REDDIT_ADS_CUSTOM_AUDIENCE_HISTORY
  
  UNION ALL
  
  SELECT 'geolocation' AS table_name, COUNT(DISTINCT geolocation_id) AS workspace_count
  FROM REDDIT_DBT_MODEL_GENERATION_PROD.WORKSPACE_MARKETING.WK_REDDIT_ADS_GEOLOCATION
  
  UNION ALL
  
  SELECT 'interest' AS table_name, COUNT(DISTINCT interest_id) AS workspace_count
  FROM REDDIT_DBT_MODEL_GENERATION_PROD.WORKSPACE_MARKETING.WK_REDDIT_ADS_INTEREST
  
  UNION ALL
  
  SELECT 'time_zone' AS table_name, COUNT(DISTINCT time_zone_id) AS workspace_count
  FROM REDDIT_DBT_MODEL_GENERATION_PROD.WORKSPACE_MARKETING.WK_REDDIT_ADS_TIME_ZONE
  
  -- Composite primary key tables(複合主キーテーブル)
  UNION ALL
  
  SELECT 'ad_group' AS table_name, COUNT(DISTINCT CONCAT(ad_group_account_id, '|', ad_group_id)) AS workspace_count
  FROM REDDIT_DBT_MODEL_GENERATION_PROD.WORKSPACE_MARKETING.WK_REDDIT_ADS_AD_GROUP
  
  UNION ALL
  
  SELECT 'targeting_community' AS table_name, COUNT(DISTINCT CONCAT(targeting_community_ad_group_id, '|', targeting_community_id)) AS workspace_count
  FROM REDDIT_DBT_MODEL_GENERATION_PROD.WORKSPACE_MARKETING.WK_REDDIT_ADS_TARGETING_COMMUNITY
  
  UNION ALL
  
  SELECT 'targeting_custom_audience' AS table_name, COUNT(DISTINCT CONCAT(targeting_custom_audience_ad_group_id, '|', targeting_custom_audience_id)) AS workspace_count
  FROM REDDIT_DBT_MODEL_GENERATION_PROD.WORKSPACE_MARKETING.WK_REDDIT_ADS_TARGETING_CUSTOM_AUDIENCE
  
  UNION ALL
  
  SELECT 'targeting_device' AS table_name, COUNT(DISTINCT targeting_device_fivetran_id) AS workspace_count
  FROM REDDIT_DBT_MODEL_GENERATION_PROD.WORKSPACE_MARKETING.WK_REDDIT_ADS_TARGETING_DEVICE
  
  UNION ALL
  
  SELECT 'targeting_geolocation' AS table_name, COUNT(DISTINCT CONCAT(targeting_geolocation_ad_group_id, '|', targeting_geolocation_id)) AS workspace_count
  FROM REDDIT_DBT_MODEL_GENERATION_PROD.WORKSPACE_MARKETING.WK_REDDIT_ADS_TARGETING_GEOLOCATION
  
  UNION ALL
  
  SELECT 'targeting_interest' AS table_name, COUNT(DISTINCT CONCAT(targeting_interest_ad_group_id, '|', targeting_interest_id)) AS workspace_count
  FROM REDDIT_DBT_MODEL_GENERATION_PROD.WORKSPACE_MARKETING.WK_REDDIT_ADS_TARGETING_INTEREST
)



-- Final comparison with validation results(検証結果との最終比較)


SELECT 
  r.table_name,
  r.raw_count,
  w.workspace_count,
  r.raw_count - w.workspace_count AS count_difference,
  CASE 
    WHEN r.raw_count = w.workspace_count THEN '✅ PASS(成功)'
    WHEN r.raw_count > w.workspace_count THEN '⚠️ RAW > WORKSPACE (Expected due to deduplication 重複除去のため正常)'
    ELSE '❌ FAIL(失敗) - WORKSPACE > RAW (Unexpected 異常)'
  END AS validation_status,
  ROUND((w.workspace_count::FLOAT / r.raw_count::FLOAT) * 100, 2) AS data_retention_percentage
FROM raw_counts r


JOIN workspace_counts w ON r.table_name = w.table_name


ORDER BY r.table_name;

クエリ結果テーブル

このクエリを実行した結果:

  • 重複除去後の行数に差異なし

  • 全テーブルで100%のデータ保持

  • すべてのテストが正常に完了

結論:大幅な時間短縮

  • 従来のアプローチ: 手動コーディング、テスト、デバッグで6-8時間

  • GitLab Duoを活用: 生成 + レビュー時間で6-8分

開発効率が60倍に向上(6-8時間から6-8分へ)し、高品質なコードを確保できます。

成功のためのベストプラクティス

本事例から得られた主要な推奨事項:

メタデータの準備

  • データ型や制約を含む完全なテーブル構造を抽出する

  • 主キーと関係を事前に特定する

  • クリーンで適切にフォーマットされたCSV入力ファイルをエクスポートする

注: GitLab DuoをMCP経由で(メタ)データに接続することで、この手動ステップを省略できる可能性があります。

明確なコンテキストの提供

  • 可能な限り既存のMR例を参照例として示す

  • コーディング標準とスタイルガイドを明示する

  • フォルダー構造と命名規則について明確に指定する

徹底的な検証

  • データ整合性を確認する検証クエリを必ず作成する

  • マージ前にローカルでテストする

  • CI/CDパイプラインを実行して問題を検出する

AIを活用したフォローアップ作業

  • テストクエリを自動生成する

  • ドキュメントテンプレートを作成する

  • 検証スクリプトを構築する

今後の展開

本ガイドでは、GitLab DuoをはじめとするAI搭載開発ツールがデータエンジニアリングワークフローをいかに変革しているかを示しました。テスト、ドキュメント、適切な構造を含む数百行の本番対応コードを数分で生成できる能力は、反復的な開発タスクへのアプローチの根本的な転換を表しています。

AIにdbtモデル作成の反復作業を委ねることで、データエンジニアはデータモデリング戦略、パフォーマンス最適化、ビジネスロジック実装などといった、より価値の高い業務に専念できます。

ぜひお試しください 小さなデータセットから着手し、メタデータを慎重に準備することで、GitLab Duoが何時間の作業をわずか数分の自動生成に変える様子を実感できます。

GitLab Duo Agent Platformを今すぐお試しください。

さらに読む

ご意見をお寄せください

このブログ記事を楽しんでいただけましたか?ご質問やフィードバックがあればお知らせください。GitLabコミュニティフォーラムで新しいトピックを作成してあなたの声を届けましょう。
Share your feedback

フォーチュン100企業の50%以上がGitLabを信頼

より優れたソフトウェアをより速く提供

インテリジェントなDevSecOpsプラットフォームで

チームの可能性を広げましょう。