<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xmlns:webfeeds="http://webfeeds.org/rss/1.0">
  <title>DataEngineering カテゴリ | フューチャー技術ブログ</title>
  <subtitle>DataEngineering カテゴリの記事一覧</subtitle>
  <icon>https://future-architect.github.io/feed_icon.png</icon>
  <logo>https://future-architect.github.io/apple-touch-icon.png</logo>
  <webfeeds:icon>https://future-architect.github.io/apple-touch-icon.png</webfeeds:icon>
  <webfeeds:accentColor>258fb8</webfeeds:accentColor>
  <link href="https://future-architect.github.io/categories/DataEngineering/atom.xml" rel="self"/>
  <link href="https://future-architect.github.io/categories/DataEngineering/"/>
  <updated>2026-07-22T15:00:00.000Z</updated>
  <id>https://future-architect.github.io/categories/DataEngineering/</id>
  <generator uri="https://hexo.io/">Hexo</generator>
  <entry>
    <title>データガバナンス設計ガイドラインを公開しました</title>
    <link href="https://future-architect.github.io/articles/20260723a/"/>
    <id>https://future-architect.github.io/articles/20260723a/</id>
    <published>2026-07-22T15:00:00.000Z</published>
    <updated>2026-07-22T15:00:00.000Z</updated>
    <author><name>真野隼記</name></author>
    <content type="html"><![CDATA[<img fetchpriority="high" src="/images/2026/20260723a/5a52d8c3-a5e9-43cf-bf30-26c78009eb7e.jpg" alt="" width="1024" height="559">

<p>データエンジニアリング連載 の5本目です。</p>
<h2 id="はじめに">はじめに</h2><p>TIG（Technology Innovation Group）の真野です。</p>
<p>このたび、フューチャーが公開しているアーキテクチャガイドライン群に、新しく「データガバナンス設計ガイドライン」を追加公開しました。</p>
<p>本記事では、なぜ今データガバナンスなのか、そしてこのガイドラインをどのような方に読んでいただきたいかを紹介します。</p>
<h2 id="なぜ今、データガバナンスなのか">なぜ今、データガバナンスなのか</h2><p>生成AIの活用が実験段階から業務適用へと進む中で、成果を分けているのはモデルの選定よりも、AIに渡すデータの側ではないでしょうか。RAGで社内ドキュメントを検索させようにも、どこに正のデータがあるか分からない、同じ指標なのに部署ごとに数字が違う、そもそも渡してよいデータなのか判断できない。このあたりで足踏みしているケースは少なくない気がします。</p>
<p>データガバナンスは従来、コンプライアンスやセキュリティといった「守り」の文脈で語られがちでした。しかしAI活用が前提となった今は、データを使える状態に保つための「攻め」の基盤整備という側面が強くなっているわけです。</p>
<h2 id="データの品質と構造は競争力になる">データの品質と構造は競争力になる</h2><p>AIモデル自体は各社が同じものを使えるため、差別化の源泉にはなりにくく、差がつくのは自社にしかないデータをどれだけ使える状態にしているかです。データの意味がメタデータとして記述され、品質基準が運用に乗り、アクセス可否が分類ルールで即断できる。この状態を作っておけば、新しいAI技術が登場するたびに準備で数か月を費やすことなく、すぐに乗ることができます。</p>
<p>逆に言うと、ガバナンスへの投資はAI活用の立ち上がり速度への投資でもある、と捉えられるのではないでしょうか。</p>
<h2 id="ガイドラインで何を扱っているか">ガイドラインで何を扱っているか</h2><p>詳細は本体に譲りますが、全体像を掴んでいただくためにキーワードを挙げます。</p>
<ul>
<li><strong>ガバナンス推進体制</strong>: 攻めのガバナンスと守りのガバナンス、データオーナー・データスチュワード、成熟度モデル</li>
<li><strong>データ分類</strong>: L1〜L4のデータ分類とアクセス管理、マスキング・匿名化</li>
<li><strong>メタデータ・データ品質</strong>: データカタログ、品質基準の定義と形骸化を防ぐ運用</li>
<li><strong>マスタデータ管理・データアーキテクチャ</strong>: 統合戦略、データ基盤の設計方針、データライフサイクル</li>
<li><strong>技術トレンドとの付き合い方</strong>: データメッシュ、Zero ETL、レイクハウス、Data Contract</li>
<li><strong>AI-Readyなガバナンス</strong>: RAG、生成AI時代に向けたデータ整備</li>
</ul>
<p>一貫しているのは、ガバナンスを「ルール作り」ではなく「品質・統制・利便性をどう意思決定するかの枠組み」として捉えている点です。最初から重厚な統制を敷くのではなく、最小限のルールから始めて段階的に育てるボトムアップのアプローチを推しています。</p>
<p>既存のデータマネジメント設計ガイドラインとの棲み分けは、ガバナンスが「立法・司法」、マネジメントが「行政」という三権分立の整理です。方針・ルールを決める側と、それを実務に落とす側で対になっているので、セットで読むと立体的に掴めると思います。</p>
<h2 id="現場の実践知の持ち寄りで作りました">現場の実践知の持ち寄りで作りました</h2><p>このガイドラインは、社内で有志メンバーを募り、週次30分の定例を2か月ほど続けるアセット活動として作成しました。メンバーはそれぞれ別の現場でデータ基盤やガバナンス推進に携わっており、各プロジェクトでの進め方を情報交換しながら、共通項を方針として言語化していくスタイルです。</p>
<p>キックオフ前のブレストの時点で、以下のような教科書ではあまり正面から扱われない論点が次々に挙がりました。</p>
<ul>
<li>「データ提供側にインセンティブがなく協力を得にくい」</li>
<li>「コーポレート部門と事業部門のどちらが主導すべきか」</li>
<li>「どのデータに履歴を持たせ、どれは最新断面だけでよいのか」</li>
</ul>
<p>このガイドラインには、こうした現場発の問いに対する現時点での回答集という性格もあります（「そもそもデータマネジメントとの違いは何か」という問いが何度も出てきたのも、この領域らしいところです）。</p>
<h2 id="どんな人に読んでほしいか">どんな人に読んでほしいか</h2><p>このガイドラインは、次のような立場の方を読者として想定しています。</p>
<ul>
<li><strong>データマネジメント組織の立ち上げ・運営を任された方</strong>: 何から手を付けるべきか、体制と役割をどう定義するか、の初手から書いています</li>
<li><strong>DX・AI活用を推進していて、PoCの先に進めず悩んでいる方</strong>: 足踏みの原因がデータの散在や品質にあるなら、その解きほぐし方のヒントになると思います</li>
<li><strong>データ基盤を構築するエンジニア・アーキテクト</strong>: 技術選定だけでなく、その基盤を組織にどう根付かせるかという運用面の設計指針として使えます</li>
<li><strong>データへの投資判断を担う経営層・マネジメント</strong>: なぜガバナンスに投資するのか、経営への説明材料としても読める構成にしています</li>
</ul>
<p>「うちはまだデータ活用の初期段階だから早い」と感じる方こそ、対象読者だと考えています。ボトムアップで小さく始める前提で書かれているため、整備が進んでからではなく、これから整備する段階で読むのが一番効果的なわけです（整備し終えた後に読むと、手戻りに気づいて胃が痛くなるかもしれません）。</p>
<h2 id="さいごに">さいごに</h2><p>データガバナンスは地味な領域と見られがちですが、AI活用の成否を裏側で決めるテーマになってきたと感じています。まずは自分の組織に近い章を1つ拾い読みするところから始めれば良いと思います。一緒に議論・執筆いただいた有志メンバーの名前は、ガイドライン末尾の謝辞に記載しています。</p>
<p>ガイドラインは公開してからが本番なので、実務で使ってみた感想やフィードバックをいただけると嬉しいです。微力ながら、各社のデータ活用の立ち上がりに貢献できれば良いなと思っています。</p>
<p>https://future-architect.github.io/arch-guidelines/documents/forDataGovernance/data_governance_guidelines.html</p>
]]></content>
    <summary type="html">フューチャーが公開しているアーキテクチャガイドライン群に、データガバナンス設計ガイドラインを追加公開しました。</summary>
    <category term="DataEngineering" scheme="https://future-architect.github.io/categories/DataEngineering/"/>
    <category term="ガイドライン" scheme="https://future-architect.github.io/tags/%E3%82%AC%E3%82%A4%E3%83%89%E3%83%A9%E3%82%A4%E3%83%B3/"/>
    <category term="データガバナンス" scheme="https://future-architect.github.io/tags/%E3%83%87%E3%83%BC%E3%82%BF%E3%82%AC%E3%83%90%E3%83%8A%E3%83%B3%E3%82%B9/"/>
    <category term="データマネジメント" scheme="https://future-architect.github.io/tags/%E3%83%87%E3%83%BC%E3%82%BF%E3%83%9E%E3%83%8D%E3%82%B8%E3%83%A1%E3%83%B3%E3%83%88/"/>
  </entry>
  <entry>
    <title>仕組みからわかる dbt Slim CI — GitHub Actions × Google Cloud Storage で実現する差分実行</title>
    <link href="https://future-architect.github.io/articles/20260706a/"/>
    <id>https://future-architect.github.io/articles/20260706a/</id>
    <published>2026-07-05T15:00:00.000Z</published>
    <updated>2026-07-05T15:00:00.000Z</updated>
    <author><name>片岡久人</name></author>
    <content type="html"><![CDATA[<img fetchpriority="high" src="/images/2026/20260706a/top.jpg" alt="" width="512" height="279">

<h2 id="1-はじめに">1. はじめに</h2><p>こんにちは。製造エネルギーグループの片岡久人です。</p>
<p>データエンジニアリング技術連載の一環として、dbt の CI を効率化する <strong>Slim CI</strong> を、<strong>dbt Core + BigQuery + GitHub Actions + Google Cloud Storage（以下 GCS）</strong> の構成で、<strong>どうやって実現するのか</strong>を解説します。</p>
<p>dbt でデータ基盤を育てていくと、ある時期から「CI が遅い」「BigQuery のコストがじわじわ増えている」という悩みにぶつかります。モデルが増えるほど、Pull Request（以下 PR）を出すたびに走る CI の時間とコストが積み上がっていくからです。Slim CI は、この悩みに対する定番の解法です。</p>
<p>Slim CI が実現すると、CI はこんなふうに動きます。</p>
<ul>
<li>PR を出すと、<strong>変更したモデルとその下流だけ</strong>が run &#x2F; test される</li>
<li>変更と無関係なモデルには触らないので、CI の時間も BigQuery のスキャンコストも「変更の規模」に比例するようになる</li>
<li>モデルが数百個に増えても、CI が線形に遅くなっていかない</li>
</ul>
<p>本記事のゴールは、この Slim CI が <strong>どのような仕組みで・どうやって実現されているのか</strong> を理解することです。Slim CI は便利なコマンドを並べれば動くものではなく、<code>manifest</code> という状態ファイルを「いつ・どこで・誰が更新するか」という設計が本体です。そのため前半では、コマンドの前に「頭の中のモデル」をしっかり作っていきます。</p>
<h3 id="前提">前提</h3><p>本記事は以下の環境を前提としています。</p>
<ul>
<li>dbt Core（<code>dbt-bigquery</code> アダプタ）を利用している。<strong>本記事は dbt Core 1.8 系を前提</strong>とします（<code>state:modified</code> や <code>--defer</code> の挙動、unit test などはバージョンによって差があるため）</li>
<li>データウェアハウスは BigQuery</li>
<li>ソースコードは GitHub で管理し、CI は GitHub Actions を利用する</li>
<li>GCP プロジェクトを操作できる</li>
</ul>
<h3 id="用語のおさらい">用語のおさらい</h3><p>本記事で当たり前のように使う dbt の3つの言葉だけ、先に確認しておきます（普段 dbt を触っている方は読み飛ばしてください）。</p>
<ul>
<li><strong>モデル（model）</strong>：1つの SQL ファイル &#x3D; 1つのテーブル &#x2F; ビュー。dbt はこれを BigQuery 上に作成します。</li>
<li><code>ref()</code>：モデルの中で別のモデルを参照する書き方です。<code>select * from &#123;&#123; ref('stg_orders') &#125;&#125;</code> のように書くと、dbt は「このモデルは <code>stg_orders</code> に依存している」と理解します。</li>
<li><strong>テスト（test）</strong>：「この列は NULL でない」「この列は一意」といった、データが満たすべき条件のチェックです。</li>
</ul>
<p>この <code>ref()</code> が、次章の主役になります。</p>
<h2 id="2-なぜ-Slim-CI-が必要なのか">2. なぜ Slim CI が必要なのか</h2><p>構築を始める前に、「そもそも何が問題で、Slim CI が何を解決するのか」をはっきりさせておきます。ここが曖昧なままだと、後半の設計判断（なぜ manifest を GCS に置くのか、なぜ CD が成功したときだけ更新するのか）が、ただの丸暗記になってしまうからです。</p>
<h3 id="CI-が無い世界">CI が無い世界</h3><p>dbt プロジェクトに CI が何も無いと、レビューは「SQL を目で追って、たぶん大丈夫」で進みます。問題は、SQL は文法的に正しくても、<strong>実際に流してみないと壊れているかどうか分からない</strong>ことです。</p>
<ul>
<li>変更したモデルの列名を変えたら、それを参照していた下流のモデルが軒並み動かなくなった</li>
<li>ある列に想定外の NULL が混ざり、その列を使う集計が静かに間違った値を出し続けた</li>
</ul>
<p>こうした問題は「マージして本番で動かして初めて発覚する」ことになります。データの怖いところは、<strong>エラーで止まらず、間違ったまま動き続ける</strong>ケースがあることです。だからこそ「マージ前に、実際に BigQuery で流して検証する」CI が欲しくなります。</p>
<h3 id="素朴な-CI（毎回フルビルド）の限界">素朴な CI（毎回フルビルド）の限界</h3><p>では素直に、「PR のたびに <code>dbt build</code>（全モデルを run + test）する」CI を組んだとします。正しさは担保できますし、最初はこれで十分です。</p>
<p>問題は、プロジェクトが育つと、この素朴な CI が3つの課題で効いてくることです。順番に見ていきましょう。</p>
<h4 id="課題（1）-時間-—-DAG-を毎回まるごと作り直している">課題（1） 時間 — DAG を毎回まるごと作り直している</h4><p>ここで dbt の一番大事な性質を確認します。1章で触れた <code>ref()</code> によって、モデルたちは<strong>お互いに依存し合ったグラフ</strong>を作ります。</p>
<pre class="mermaid" data-mermaid="3debbb9a07df89d753b0b13f8a8a0caac54bfb0266874411de6c55ac188a7e1b">flowchart LR
  stg_orders --> int_sales
  stg_items --> int_sales
  int_sales --> mart_sales_daily --> mart_sales_report</pre>

<p>矢印は「A を作ってから B を作る」という依存関係です。このグラフは循環しない（下流から上流へ戻らない）ので、<strong>DAG（有向非巡回グラフ）</strong> と呼ばれます。dbt はこの DAG を読み取り、正しい順番でモデルを作っていきます。</p>
<p><code>dbt build</code> は、この <strong>DAG 全体を毎回まるごと作り直します</strong>。PR で触ったのが1モデルだけでも、関係ない残り全部もビルドします。</p>
<p>モデルが N 個あれば、PR の中身に関係なく毎回 N 個ビルドすることになります。プロジェクトが育つ &#x3D; N が増える &#x3D; <strong>CI 時間が N に比例して伸びていく</strong>。やがて「PR を出してから CI が終わるまで待たされる時間」が、レビューのボトルネックになっていきます。</p>
<h4 id="課題（2）-お金-—-BigQuery-はスキャンした分だけ課金される">課題（2） お金 — BigQuery はスキャンした分だけ課金される</h4><p>BigQuery の料金は、オンデマンド課金の場合、<strong>クエリがスキャンしたデータ量</strong>で決まります。全モデルを build するということは、PR のたびに全ソーステーブルを読み直すということです。</p>
<p>1回あたりは小さく見えても、開発が活発なチームでは <strong>PR 回数 × 毎回フルスキャン</strong>で積み上がっていきます。「変更と1ミリも関係ないモデルのスキャン料を、PR のたびに払い続けている」状態です。</p>
<h4 id="課題（3）-ノイズ-—-CI-の赤が信用されなくなる">課題（3） ノイズ — CI の赤が信用されなくなる</h4><p>全モデルを test すると、<strong>自分の変更とは無関係なモデルのデータ起因の失敗</strong>まで拾ってしまいます。上流データの一時的な乱れで、自分が触ってもいないモデルのテストが赤くなる、といったことが起こります。</p>
<p>これが続くと、「CI はどうせたまに赤い」という空気になり、<strong>本当に自分の変更が何かを壊した赤</strong>が、そのノイズに埋もれて見逃されるようになります。</p>
<p>CI の価値は速さだけではありません。<strong>「赤 &#x3D; 自分の変更が何かを壊した」と信じられること</strong>、このシグナルの信頼性こそが本質です。全量 test はこの信頼性を下げてしまいます。</p>
<h3 id="では、何を検証すれば「十分」なのか">では、何を検証すれば「十分」なのか</h3><p>3つの課題の根っこは、すべて「<strong>変更と関係ないところまで検証している</strong>」ことにあります。ならば逆に問い直してみます。PR を安全にマージするために、最小限どこまで検証すれば十分でしょうか。</p>
<p>答えは、<strong>変更の影響範囲だけ</strong>です。具体的には次の2つです。</p>
<ol>
<li><strong>変更したモデルそのもの</strong> — ちゃんとビルドでき、テストを通るか</li>
<li><strong>その下流のモデル全部</strong> — <code>ref()</code> を辿った先。上流を変えた影響で壊れないか</li>
</ol>
<p>先ほどの DAG でいうと、<code>int_sales</code> を変更したなら、検証すべきは <code>int_sales</code> とその下流の <code>mart_sales_daily</code>・<code>mart_sales_report</code> です。</p>
<pre class="mermaid" data-mermaid="7ce264ad694b6b7826043b55cb86cfb197b2b8bef359e449c604a78b07a9d6bb">flowchart LR
  stg_orders --> int_sales
  stg_items --> int_sales
  int_sales --> mart_sales_daily --> mart_sales_report

  classDef target fill:#ffe0b2,stroke:#e65100,color:#000;
  class int_sales,mart_sales_daily,mart_sales_report target;</pre>

<p>一方で<strong>上流</strong>（<code>stg_orders</code>・<code>stg_items</code>）はどうでしょうか。これらは今回変更していません。変更していない &#x3D; <strong>すでに本番で動いて検証済み</strong>ということです。作り直す必要はなく、「本番にある実テーブルをそのまま参照」すれば十分です。</p>
<p>この2つの発想——</p>
<ul>
<li>検証するのは「変更モデル + その下流」だけ</li>
<li>変更していない上流は、本番の実テーブルを参照して済ませる</li>
</ul>
<p>——が、Slim CI の考え方そのものです。そして次章で見るように、これらはそれぞれ dbt の <code>state:modified+</code> と <code>--defer</code> というフラグに、ほぼそのまま対応します。</p>
<div class="note-container note-info"><span class="note-icon"></span><div>

<p>dbt Cloud にはこの Slim CI が機能として組み込まれています。本記事が対象にしているのは <strong>dbt Core を自前の CI（GitHub Actions）で動かす</strong>ケースです。この場合、Slim CI の心臓である「比較の基準となる manifest」を、自分たちで保存・受け渡しする仕組みを作る必要があります。次章以降は、まさにその設計と実装の話です。</p>
</div></div>

<h2 id="3-Slim-CI-の仕組み">3. Slim CI の仕組み</h2><p>2章で「変更モデル + その下流だけ検証し、上流は本番の実テーブルを参照する」という方針にたどり着きました。この章では、それを dbt がどうやって実現しているのかを見ていきます。ここが理解できれば、あとの YAML は「その仕組みを GitHub Actions で動かしているだけ」に見えてきます。</p>
<h3 id="manifest-とは何か">manifest とは何か</h3><p>Slim CI を理解する鍵は、<code>manifest.json</code>（以下 manifest）というファイルです。</p>
<p>manifest は、dbt が <code>dbt compile</code>（や <code>run</code> などコンパイルを伴うコマンド）のときに <code>target/</code> ディレクトリに吐き出す、<strong>プロジェクトの設計図</strong>です。中には、全モデルの一覧・それぞれの依存関係（DAG）・コンパイル後の SQL などが、まるごと記録されています。</p>
<p>つまり manifest は「その時点のプロジェクトのスナップショット」です。この性質が、次の state 比較に効いてきます。</p>
<h3 id="state-比較-—-manifest-版の-git-diff">state 比較 — manifest 版の git diff</h3><p>Slim CI がやりたいのは「何が変わったモデルなのか」を知ることです。dbt はこれを、<strong>2つの manifest を突き合わせる</strong>ことで実現します。</p>
<ul>
<li>基準となる manifest：<strong>前回デプロイが成功したときの設計図</strong>（＝本番の状態）</li>
<li>今の manifest：<strong>この PR での設計図</strong></li>
</ul>
<p>この2つを比べて、SQL や設定に差分のあるモデルを「変更されたモデル」として特定します。イメージとしては <strong>manifest 版の <code>git diff</code></strong> です。git がファイルの差分を見るのに対し、dbt はモデルの差分を見ている、と考えると腹落ちしやすいと思います。</p>
<h3 id="3つのフラグ">3つのフラグ</h3><p>この state 比較を実際のコマンドにするのが、次の3つのフラグです。2章の方針と1対1で対応しています。</p>
<ul>
<li><strong><code>--select state:modified+</code></strong><br>基準 manifest と比べて「変更されたモデル」＋「その下流」を選択します。末尾の <code>+</code> が「下流も含める」の意味です。2章でいう変更の影響範囲が、そのままコマンドになったものです。</li>
<li><strong><code>--state &lt;ディレクトリ&gt;</code></strong><br>比較基準にする manifest（前回デプロイ成功時のもの）が置いてある場所を指定します。</li>
<li><strong><code>--defer</code></strong><br>今回ビルドしない上流モデルを、<strong>CI 環境で作り直す代わりに、すでにビルド済みの環境（本番）にある実テーブルで代用する</strong>ようにします。<br>もう少し正確に言うと、dbt は普段 <code>ref(&#39;stg_orders&#39;)</code> を「自分が実行している環境の <code>stg_orders</code>」と読み替えます。<code>--defer</code> を付けると、今回選ばれなかったモデルについては、<strong>まず実行中の環境（<code>ci</code>）に同名のテーブルがあるか探し、無ければ基準（<code>--state</code> で指定した本番）側のテーブルを参照する</strong>ようになります。クリーンな CI 環境なら上流は存在しないので、結果として本番が参照され、変更していない上流をわざわざ作り直さずに済みます。</li>
</ul>
<p>そして重要なのは、<strong>この3つはセットで初めて成立する</strong>ということです。仮に <code>--defer</code> が無いと、<code>state:modified+</code> で選ばれなかった上流モデルが CI 環境に存在せず、参照先が見つからずに落ちてしまいます。「変更分だけ作り、残りは本番を借りる」——この合わせ技が Slim CI の肝です。</p>
<div class="note-container note-info"><span class="note-icon"></span><div>

<p>本記事では説明をシンプルにするため、比較の基準（<code>--state</code>）も <code>--defer</code> の参照先も <strong>本番</strong> として書いています。ただし、どの環境を基準にするかはチームの運用や「何を検証したいか」によって変わります。たとえば「開発環境にマージする前に、必要な差分テストを洗い出す」ことが目的なら、その<strong>開発環境</strong>を基準・参照先にする、といった構成も考えられます（基準となる manifest も、環境ごとに用意することになります）。読むときは「本番」を、自分の環境における“安定した基準となる状態”に読み替えてください。</p>
</div></div>

<div class="note-container note-info"><span class="note-icon"></span><div>

<p>理屈のうえでは、<code>ci</code> のような固定のデータセットを使い回していると、過去の PR 実行で作られた古いテーブルが <code>ci</code> に残っていた場合に、<code>--defer</code> が本番ではなくそちらを参照する、ということも起こり得るようです。気になる場合は、<code>--favor-state</code> を付けると常に基準（<code>--state</code>）側のテーブルを優先させられます。</p>
</div></div>

<h3 id="manifest-の循環-—-Slim-CI-の心臓">manifest の循環 — Slim CI の心臓</h3><p>ここまでで「基準となる manifest（前回デプロイ成功時の設計図）」が必要だと分かりました。では、その基準はどこから来て、いつ更新されるのでしょうか。</p>
<p>答えが、次の循環です。</p>
<pre class="mermaid" data-mermaid="15a53be30e787e65b3ac0cd2ab3176e280e6af0c37f48d8ccc779b002100e40a">flowchart LR
  B[("GCS上の<br/>manifest.json<br/>（比較の基準）")]

  subgraph pr["PR作成・更新時（slim-ci.yml）"]
    A["① 基準manifestを取得"] --> R["② state:modified+ で<br/>変更モデル＋下流だけ<br/>run / test"]
  end

  subgraph merge["mainへマージ時（deploy.yml）"]
    E["③ 差分をbuildして本番反映"] --> U["④ 成功したら<br/>最新manifestを<br/>アップロード"]
  end

  B -->|取得| A
  U -->|次回PRの基準になる| B</pre>

<ul>
<li><strong>PR のとき</strong>：GCS から基準 manifest を取ってきて、それと比べて変更分だけ検証する</li>
<li><strong>マージのとき</strong>：本番に反映したあと、その最新状態の manifest を GCS にアップロードし直す</li>
<li>こうして上げられた manifest が、<strong>次の PR の比較基準</strong>になる</li>
</ul>
<p>この「マージのたびに基準が最新化され、次の PR がそれを参照する」というぐるぐる回る仕組みこそが、Slim CI の心臓部です。</p>
<h3 id="manifest-の置き場所-—-GCS-か、GitHub-Actions-の-Artifact-か">manifest の置き場所 — GCS か、GitHub Actions の Artifact か</h3><p>「基準 manifest をどこに置くか」には、大きく2つの選択肢があります。<strong>GCS のような外部ストレージ</strong>に置くか、<strong>GitHub Actions の Artifact</strong>（ワークフローの成果物置き場）に置くかです。それぞれ一長一短があります。</p>
<p><strong>GitHub Actions の Artifact に置く場合</strong></p>
<ul>
<li>メリット：GitHub の中で完結する。GCS バケットのような外部リソースや、それに対する権限を用意しなくてよい</li>
<li>デメリット：Artifact には保存期限があり（デフォルト90日）、期限管理の考え方が GitHub 依存になる。また、「main の最新の成功デプロイ実行から manifest を拾ってくる」ために、<strong>その実行の <code>run-id</code> を特定する一手間がかかる</strong>（<code>actions/download-artifact@v4</code> は <code>run-id</code> と <code>github-token</code> を渡せば実行をまたいで取得できるが、どの実行が最新の成功デプロイかは <code>workflow_run</code> トリガーや <code>gh</code>&#x2F;API で解決する必要がある）。GCS の固定パスなら、この run-id 特定の手間がそもそも要らない</li>
</ul>
<p><strong>GCS に置く場合</strong></p>
<ul>
<li>メリット：<code>gs://.../manifest.json</code> という固定パスが常に「最新の基準」を指すので、どのワークフローからでも素直に取得できる。保持期間も自分で制御できる。さらに、<strong>本記事では dbt が生成するドキュメント（dbt docs）も同じ GCS バケットで配信する構成</strong>にしており、manifest と docs の置き場所を一本化できる（この docs 配信は6章の <code>deploy.yml</code> で実際に行います）</li>
<li>デメリット：GCS バケットという管理対象と、それに対する権限設定が増える</li>
</ul>
<p>本記事で GCS を採用しているのは、正直なところ <strong>BigQuery を使っている時点で GCP プロジェクトが手元にあり、素直に組めたから</strong>という面が大きいです。「実行をまたいだ受け渡しのしやすさ」と「dbt docs を GCS でホストして manifest と同居させられること」を重く見るなら GCS が扱いやすく、「GitHub の外にリソースを増やしたくない」を重く見るなら Artifact も十分に選択肢になります。自分のチームがどちらを重視するかで選ぶとよいと思います。</p>
<h2 id="4-どうやって実現するか-—-全体像と必要な部品">4. どうやって実現するか — 全体像と必要な部品</h2><p>仕組みが分かったところで、それを GitHub Actions 上で動かすために必要な「登場人物」を整理します。この章はコマンドを一つひとつ再現する手順書ではなく、<strong>実現の地図</strong>として読んでください。</p>
<h3 id="必要な部品">必要な部品</h3><p>Slim CI を回すには、ざっくり次の部品がそろっていれば OK です。</p>
<ul>
<li><strong>GCS バケット 1つ</strong>：基準 manifest の置き場所。</li>
<li><strong>GitHub Actions から GCP への認証</strong>：ワークフローが BigQuery や GCS を操作するための認証です。<code>google-github-actions/auth</code> を使って設定します（具体的な認証方式の設定は本記事の本筋から外れるため割愛します）。</li>
<li><strong>CI 用・本番用の権限（サービスアカウント／Workload Identity）</strong>：本記事のワークフローは、サービスアカウントを Workload Identity 経由で利用する前提です。ここに1つ勘所があります。CI 側の権限には、<strong>基準となる環境（本記事では本番）のデータセットの読み取り権限</strong>が必要です。<code>--defer</code> で上流モデルをその環境のテーブルから参照するためで、これを忘れると PR のときだけ「テーブルが見つからない」で落ちます。</li>
<li><strong>dbt の <code>profiles.yml</code> に2つのターゲット</strong>：CI 実行用のターゲットと、本番デプロイ実行用のターゲット（<code>deploy.yml</code> の <code>dbt run --target prod</code> で使います）。なお <code>--defer</code> の参照先そのものは、この prod ターゲットではなく <code>--state</code> に渡す manifest に記録された本番のテーブル情報で決まります。</li>
</ul>
<figure class="highlight yaml"><table><tr><td class="code"><pre><span class="line"><span class="comment"># profiles.yml（抜粋）</span></span><br><span class="line"><span class="attr">my_dbt_project:</span></span><br><span class="line">  <span class="attr">target:</span> <span class="string">ci</span></span><br><span class="line">  <span class="attr">outputs:</span></span><br><span class="line">    <span class="attr">ci:</span> <span class="comment"># CI 実行用。専用データセットに書き込む</span></span><br><span class="line">      <span class="attr">type:</span> <span class="string">bigquery</span></span><br><span class="line">      <span class="attr">method:</span> <span class="string">oauth</span> <span class="comment"># 実行環境の認証情報を利用（キーファイル不要）</span></span><br><span class="line">      <span class="attr">project:</span> <span class="string">my-project-dev</span></span><br><span class="line">      <span class="attr">dataset:</span> <span class="string">ci</span></span><br><span class="line">      <span class="attr">location:</span> <span class="string">asia-northeast1</span></span><br><span class="line">      <span class="attr">threads:</span> <span class="number">8</span></span><br><span class="line">    <span class="attr">prod:</span> <span class="comment"># 本番デプロイ実行用（deploy.yml の --target prod で使う）</span></span><br><span class="line">      <span class="attr">type:</span> <span class="string">bigquery</span></span><br><span class="line">      <span class="attr">method:</span> <span class="string">oauth</span></span><br><span class="line">      <span class="attr">project:</span> <span class="string">my-project-prod</span></span><br><span class="line">      <span class="attr">dataset:</span> <span class="string">analytics</span></span><br><span class="line">      <span class="attr">location:</span> <span class="string">asia-northeast1</span></span><br><span class="line">      <span class="attr">threads:</span> <span class="number">8</span></span><br></pre></td></tr></table></figure>

<div class="note-container note-info"><span class="note-icon"></span><div>

<p>GCP への認証や IAM ロールの具体的な設定は、環境によって差が大きく、本記事の本筋からも外れるため割愛します。認証には <code>google-github-actions/auth</code> を使います。詳細は公式を参照してください。</p>
</div></div>

<p>https://github.com/google-github-actions/auth</p>
<h2 id="5-PR-側のワークフローを読む-—-slim-ci-yml">5. PR 側のワークフローを読む — slim-ci.yml</h2><p>いよいよ本体です。PR が作られた&#x2F;更新されたときに走る <code>slim-ci.yml</code> を見ていきます。全文は折りたたみに入れておくので、ここでは<strong>キモの3ステップ</strong>に絞って解説します。</p>
<div class="note-container note-warn"><span class="note-icon"></span><div>

<p>以降で載せるワークフロー（<code>slim-ci.yml</code> &#x2F; <code>deploy.yml</code>）は、仕組みを説明するための<strong>サンプル</strong>です。そのままコピーすれば動くものではなく、プロジェクト名・バケット名・ディレクトリ構成・認証まわりなどはご自身の環境に合わせて調整してください。</p>
</div></div>

<details><summary>slim-ci.yml 全文</summary>

<figure class="highlight yaml"><table><tr><td class="code"><pre><span class="line"><span class="attr">name:</span> <span class="string">dbt</span> <span class="string">Slim</span> <span class="string">CI</span></span><br><span class="line"></span><br><span class="line"><span class="attr">on:</span></span><br><span class="line">  <span class="attr">pull_request:</span></span><br><span class="line">    <span class="attr">branches:</span> [<span class="string">main</span>]</span><br><span class="line">    <span class="attr">paths:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&#x27;dbt/**&#x27;</span> <span class="comment"># dbt の変更を含む PR のときだけ起動</span></span><br><span class="line"></span><br><span class="line"><span class="attr">jobs:</span></span><br><span class="line">  <span class="attr">slim-ci:</span></span><br><span class="line">    <span class="attr">runs-on:</span> <span class="string">ubuntu-latest</span></span><br><span class="line">    <span class="attr">permissions:</span></span><br><span class="line">      <span class="attr">contents:</span> <span class="string">read</span></span><br><span class="line">      <span class="attr">id-token:</span> <span class="string">write</span> <span class="comment"># GCP への認証に必要</span></span><br><span class="line">    <span class="attr">steps:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">uses:</span> <span class="string">actions/checkout@v4</span></span><br><span class="line"></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">uses:</span> <span class="string">actions/setup-python@v5</span></span><br><span class="line">        <span class="attr">with:</span></span><br><span class="line">          <span class="attr">python-version:</span> <span class="string">&#x27;3.12&#x27;</span></span><br><span class="line"></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">uses:</span> <span class="string">google-github-actions/auth@v2</span></span><br><span class="line">        <span class="attr">with:</span></span><br><span class="line">          <span class="attr">workload_identity_provider:</span> <span class="string">$&#123;&#123;</span> <span class="string">secrets.WORKLOAD_IDENTITY_PROVIDER</span> <span class="string">&#125;&#125;</span></span><br><span class="line">          <span class="attr">service_account:</span> <span class="string">$&#123;&#123;</span> <span class="string">secrets.CI_SERVICE_ACCOUNT</span> <span class="string">&#125;&#125;</span></span><br><span class="line"></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">uses:</span> <span class="string">google-github-actions/setup-gcloud@v2</span></span><br><span class="line"></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Install</span> <span class="string">dbt</span></span><br><span class="line">        <span class="attr">run:</span> <span class="string">pip</span> <span class="string">install</span> <span class="string">&#x27;dbt-bigquery~=1.8.0&#x27;</span> <span class="comment"># 本記事の前提バージョン（1.8系）に固定</span></span><br><span class="line"></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Install</span> <span class="string">dbt</span> <span class="string">packages</span></span><br><span class="line">        <span class="attr">working-directory:</span> <span class="string">dbt</span></span><br><span class="line">        <span class="attr">run:</span> <span class="string">dbt</span> <span class="string">deps</span></span><br><span class="line"></span><br><span class="line">      <span class="comment"># ① 基準となる本番 manifest を取得</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Get</span> <span class="string">prod</span> <span class="string">manifest</span></span><br><span class="line">        <span class="attr">working-directory:</span> <span class="string">dbt</span></span><br><span class="line">        <span class="attr">run:</span> <span class="string">|</span></span><br><span class="line"><span class="string">          mkdir -p prod-run-artifacts</span></span><br><span class="line"><span class="string">          gcloud storage cp gs://my-project-dbt-artifacts/manifest.json \</span></span><br><span class="line"><span class="string">            prod-run-artifacts/manifest.json</span></span><br><span class="line"><span class="string"></span></span><br><span class="line">      <span class="comment"># ② 何が選ばれるかを可視化（動作確認・デバッグ用）</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Show</span> <span class="string">selected</span> <span class="string">models</span></span><br><span class="line">        <span class="attr">working-directory:</span> <span class="string">dbt</span></span><br><span class="line">        <span class="attr">run:</span> <span class="string">dbt</span> <span class="string">ls</span> <span class="string">--select</span> <span class="string">state:modified+</span> <span class="string">--state</span> <span class="string">prod-run-artifacts</span> <span class="string">--target</span> <span class="string">ci</span></span><br><span class="line"></span><br><span class="line">      <span class="comment"># ③ 変更モデル＋下流だけを run / test（上流は --defer で本番を参照）</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">dbt</span> <span class="string">run</span> <span class="string">&amp;</span> <span class="string">test</span> <span class="string">(modified</span> <span class="string">models</span> <span class="string">only)</span></span><br><span class="line">        <span class="attr">working-directory:</span> <span class="string">dbt</span></span><br><span class="line">        <span class="attr">run:</span> <span class="string">|</span></span><br><span class="line"><span class="string">          dbt run  --select state:modified+ --defer --state prod-run-artifacts --target ci</span></span><br><span class="line"><span class="string">          dbt test --select state:modified+ --defer --state prod-run-artifacts --target ci</span></span><br></pre></td></tr></table></figure>

</details>

<h3 id="キモ（1）-基準-manifest-を取ってくる">キモ（1） 基準 manifest を取ってくる</h3><figure class="highlight yaml"><table><tr><td class="code"><pre><span class="line"><span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Get</span> <span class="string">prod</span> <span class="string">manifest</span></span><br><span class="line">  <span class="attr">working-directory:</span> <span class="string">dbt</span></span><br><span class="line">  <span class="attr">run:</span> <span class="string">|</span></span><br><span class="line"><span class="string">    mkdir -p prod-run-artifacts</span></span><br><span class="line"><span class="string">    gcloud storage cp gs://my-project-dbt-artifacts/manifest.json \</span></span><br><span class="line"><span class="string">      prod-run-artifacts/manifest.json</span></span><br></pre></td></tr></table></figure>

<p>3章の「比較の基準」を GCS から取ってくるステップです。落としてきた manifest を <code>prod-run-artifacts/</code> に置き、次のステップで <code>--state</code> にこのディレクトリを指定します。</p>
<div class="note-container note-warn"><span class="note-icon"></span><div>

<p>このステップには順序依存があります。基準 manifest は次章の <code>deploy.yml</code> が作って GCS に置くものなので、<strong>まだ一度も deploy が走っていない初回は、ここで manifest が見つからず失敗します</strong>。最初に一度 <code>deploy.yml</code> を通して基準 manifest を用意してから、PR 側の Slim CI が使えるようになります（<code>deploy.yml</code> 側は manifest が無くても全量ビルドにフォールバックするようになっています。詳しくは6章で触れます）。</p>
</div></div>

<h3 id="キモ（2）-何が選ばれるかを可視化する">キモ（2） 何が選ばれるかを可視化する</h3><figure class="highlight yaml"><table><tr><td class="code"><pre><span class="line"><span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Show</span> <span class="string">selected</span> <span class="string">models</span></span><br><span class="line">  <span class="attr">working-directory:</span> <span class="string">dbt</span></span><br><span class="line">  <span class="attr">run:</span> <span class="string">dbt</span> <span class="string">ls</span> <span class="string">--select</span> <span class="string">state:modified+</span> <span class="string">--state</span> <span class="string">prod-run-artifacts</span> <span class="string">--target</span> <span class="string">ci</span></span><br></pre></td></tr></table></figure>

<p><code>dbt ls</code> は「選択されるモデルを一覧するだけ」のコマンドです。run&#x2F;test の前にこれを挟んでおくと、<strong>今回の PR で何が検証対象になるのか</strong>が Actions のログで一目で分かります。必須ではありませんが、仕組みを理解するうえでも、運用でのデバッグでも効くのでおすすめです。</p>
<h3 id="キモ（3）-変更分だけ-run-test-する">キモ（3） 変更分だけ run &#x2F; test する</h3><figure class="highlight yaml"><table><tr><td class="code"><pre><span class="line"><span class="bullet">-</span> <span class="attr">name:</span> <span class="string">dbt</span> <span class="string">run</span> <span class="string">&amp;</span> <span class="string">test</span> <span class="string">(modified</span> <span class="string">models</span> <span class="string">only)</span></span><br><span class="line">  <span class="attr">working-directory:</span> <span class="string">dbt</span></span><br><span class="line">  <span class="attr">run:</span> <span class="string">|</span></span><br><span class="line"><span class="string">    dbt run  --select state:modified+ --defer --state prod-run-artifacts --target ci</span></span><br><span class="line"><span class="string">    dbt test --select state:modified+ --defer --state prod-run-artifacts --target ci</span></span><br></pre></td></tr></table></figure>

<p>3章で説明した3フラグがそのまま登場しています。<code>state:modified+</code> で変更モデルと下流を選び、<code>--defer</code> で上流は本番を参照し、<code>--state</code> で基準 manifest の場所を教えている——それだけです。仕組みが分かっていれば、このコマンドはもう読めるはずです。</p>
<p>なお <code>on.paths</code> で <code>dbt/**</code> を指定しているので、dbt に関係ない PR ではこのワークフローは起動しません。無駄な CI を回さないための小さな工夫です。</p>
<h2 id="6-マージ側のワークフローを読む-—-deploy-yml">6. マージ側のワークフローを読む — deploy.yml</h2><p>PR 側だけでは循環が完成しません。マージ時に<strong>基準 manifest を最新化する</strong>のが <code>deploy.yml</code> の役割です。ここも全文は折りたたみにして、キモだけ解説します（こちらも5章と同じく、そのまま動くものではなくサンプルです）。</p>
<details><summary>deploy.yml 全文</summary>

<figure class="highlight yaml"><table><tr><td class="code"><pre><span class="line"><span class="attr">name:</span> <span class="string">dbt</span> <span class="string">Deploy</span></span><br><span class="line"></span><br><span class="line"><span class="attr">on:</span></span><br><span class="line">  <span class="attr">pull_request:</span></span><br><span class="line">    <span class="attr">types:</span> [<span class="string">closed</span>]</span><br><span class="line">    <span class="attr">branches:</span> [<span class="string">main</span>]</span><br><span class="line">    <span class="attr">paths:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&#x27;dbt/**&#x27;</span> <span class="comment"># dbt の変更を含む PR のマージ時だけ起動</span></span><br><span class="line"></span><br><span class="line"><span class="attr">jobs:</span></span><br><span class="line">  <span class="attr">deploy:</span></span><br><span class="line">    <span class="attr">if:</span> <span class="string">github.event.pull_request.merged</span> <span class="string">==</span> <span class="literal">true</span> <span class="comment"># マージされたPRのみ</span></span><br><span class="line">    <span class="attr">runs-on:</span> <span class="string">ubuntu-latest</span></span><br><span class="line">    <span class="attr">permissions:</span></span><br><span class="line">      <span class="attr">contents:</span> <span class="string">read</span></span><br><span class="line">      <span class="attr">id-token:</span> <span class="string">write</span></span><br><span class="line">    <span class="attr">steps:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">uses:</span> <span class="string">actions/checkout@v4</span></span><br><span class="line"></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">uses:</span> <span class="string">actions/setup-python@v5</span></span><br><span class="line">        <span class="attr">with:</span></span><br><span class="line">          <span class="attr">python-version:</span> <span class="string">&#x27;3.12&#x27;</span></span><br><span class="line"></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">uses:</span> <span class="string">google-github-actions/auth@v2</span></span><br><span class="line">        <span class="attr">with:</span></span><br><span class="line">          <span class="attr">workload_identity_provider:</span> <span class="string">$&#123;&#123;</span> <span class="string">secrets.WORKLOAD_IDENTITY_PROVIDER</span> <span class="string">&#125;&#125;</span></span><br><span class="line">          <span class="attr">service_account:</span> <span class="string">$&#123;&#123;</span> <span class="string">secrets.CD_SERVICE_ACCOUNT</span> <span class="string">&#125;&#125;</span></span><br><span class="line"></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">uses:</span> <span class="string">google-github-actions/setup-gcloud@v2</span></span><br><span class="line"></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Install</span> <span class="string">dbt</span></span><br><span class="line">        <span class="attr">run:</span> <span class="string">pip</span> <span class="string">install</span> <span class="string">&#x27;dbt-bigquery~=1.8.0&#x27;</span> <span class="comment"># 本記事の前提バージョン（1.8系）に固定</span></span><br><span class="line"></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Install</span> <span class="string">dbt</span> <span class="string">packages</span></span><br><span class="line">        <span class="attr">working-directory:</span> <span class="string">dbt</span></span><br><span class="line">        <span class="attr">run:</span> <span class="string">dbt</span> <span class="string">deps</span></span><br><span class="line"></span><br><span class="line">      <span class="comment"># manifest が取れなくても止めない（初回導入時は全量buildにフォールバック）</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Get</span> <span class="string">prod</span> <span class="string">manifest</span> <span class="string">(best</span> <span class="string">effort)</span></span><br><span class="line">        <span class="attr">working-directory:</span> <span class="string">dbt</span></span><br><span class="line">        <span class="attr">run:</span> <span class="string">|</span></span><br><span class="line"><span class="string">          mkdir -p prod-run-artifacts</span></span><br><span class="line"><span class="string">          gcloud storage cp gs://my-project-dbt-artifacts/manifest.json \</span></span><br><span class="line"><span class="string">            prod-run-artifacts/manifest.json \</span></span><br><span class="line"><span class="string">            || echo &quot;::warning::No prod manifest – falling back to full run&quot;</span></span><br><span class="line"><span class="string"></span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">dbt</span> <span class="string">run</span> <span class="string">(modified</span> <span class="string">models</span> <span class="string">only,</span> <span class="string">fallback</span> <span class="string">to</span> <span class="string">full)</span></span><br><span class="line">        <span class="attr">working-directory:</span> <span class="string">dbt</span></span><br><span class="line">        <span class="attr">run:</span> <span class="string">|</span></span><br><span class="line"><span class="string">          if [[ -f prod-run-artifacts/manifest.json ]]; then</span></span><br><span class="line"><span class="string">            SELECT_ARGS=&quot;--select state:modified+ --state prod-run-artifacts&quot;</span></span><br><span class="line"><span class="string">          else</span></span><br><span class="line"><span class="string">            SELECT_ARGS=&quot;&quot;</span></span><br><span class="line"><span class="string">          fi</span></span><br><span class="line"><span class="string">          dbt run --target prod $SELECT_ARGS</span></span><br><span class="line"><span class="string"></span></span><br><span class="line">      <span class="comment"># デプロイ成功時のみ manifest を更新（＝次回PRの基準を最新化）</span></span><br><span class="line">      <span class="comment"># あわせて dbt docs（静的HTML）も生成し、同じバケットで配信する</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Update</span> <span class="string">manifest</span> <span class="string">&amp;</span> <span class="string">docs</span> <span class="string">on</span> <span class="string">GCS</span></span><br><span class="line">        <span class="attr">working-directory:</span> <span class="string">dbt</span></span><br><span class="line">        <span class="attr">run:</span> <span class="string">|</span></span><br><span class="line"><span class="string">          dbt docs generate --static --target prod</span></span><br><span class="line"><span class="string">          gcloud storage cp target/manifest.json gs://my-project-dbt-artifacts/manifest.json</span></span><br><span class="line"><span class="string">          gcloud storage cp target/static_index.html gs://my-project-dbt-artifacts/index.html</span></span><br></pre></td></tr></table></figure>

</details>

<h3 id="キモ-成功したときだけ-manifest-を更新する">キモ 成功したときだけ manifest を更新する</h3><figure class="highlight yaml"><table><tr><td class="code"><pre><span class="line"><span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Update</span> <span class="string">manifest</span> <span class="string">&amp;</span> <span class="string">docs</span> <span class="string">on</span> <span class="string">GCS</span></span><br><span class="line">  <span class="attr">working-directory:</span> <span class="string">dbt</span></span><br><span class="line">  <span class="attr">run:</span> <span class="string">|</span></span><br><span class="line"><span class="string">    dbt docs generate --static --target prod</span></span><br><span class="line"><span class="string">    gcloud storage cp target/manifest.json gs://my-project-dbt-artifacts/manifest.json</span></span><br><span class="line"><span class="string">    gcloud storage cp target/static_index.html gs://my-project-dbt-artifacts/index.html</span></span><br></pre></td></tr></table></figure>

<p>一見ただのアップロードですが、このステップが <strong><code>dbt run</code> の後ろに置かれていること</strong>自体が最重要ポイントです。GitHub Actions のステップは、前のステップが失敗すると後続がスキップされるのがデフォルトの挙動です。つまり manifest のアップロードを run のあとに置くだけで、<strong>デプロイ（run）が成功したときだけ</strong>最新 manifest が GCS に上がる、が成立します。</p>
<p>なぜ「成功時だけ」でなければいけないのでしょうか。基準 manifest は「本番が今どうなっているか」を表すものです。もしデプロイが失敗したのに manifest を更新してしまうと、「実際には反映されていない変更」を反映済みとして記録してしまい、次の PR でその変更が検証対象から漏れます。逆に、失敗時に更新しなければ、その変更は次の PR でも「まだ modified」として扱われ、<strong>自然にリトライ</strong>されます。「成功したときだけ基準を最新化する」——この設計が、循環を正しく保っています。</p>
<div class="note-container note-info"><span class="note-icon"></span><div>

<p>このステップで <code>dbt docs generate</code> を使っているのには理由があります。実は、基準にしたい <code>manifest.json</code> は直前の <code>dbt run</code> の時点で <code>target/</code> に生成済みなので、<strong>manifest を上げるだけなら <code>docs generate</code> は不要</strong>です。ここで実行しているのは、<strong>dbt docs（データカタログの静的サイト）も同じ GCS バケットで配信している</strong>ためです（3章で GCS の利点として挙げた「docs のホスティングと同居できる」がこれにあたります）。<code>docs generate</code> は catalog を作るためにウェアハウスへ追加のクエリ（<code>INFORMATION_SCHEMA</code> の読み取り）を投げるので、そのぶんのひと手間はかかります。docs を配信しないなら、このステップは省いて、直前の run が生成した <code>manifest.json</code> をそのまま上げるだけで十分です。</p>
</div></div>

<h3 id="鶏と卵の問題">鶏と卵の問題</h3><p>「基準 manifest が無いと Slim CI が動かない。でも最初の manifest は誰が作るの?」という疑問が湧くかもしれません。これは、manifest の取得を <strong>best effort</strong> にし、無ければ全量 build にフォールバックすることで解決しています。</p>
<figure class="highlight yaml"><table><tr><td class="code"><pre><span class="line"><span class="string">if</span> [[ <span class="string">-f</span> <span class="string">prod-run-artifacts/manifest.json</span> ]]<span class="string">;</span> <span class="string">then</span></span><br><span class="line">  <span class="string">SELECT_ARGS=&quot;--select</span> <span class="string">state:modified+</span> <span class="string">--state</span> <span class="string">prod-run-artifacts&quot;</span></span><br><span class="line"><span class="string">else</span></span><br><span class="line">  <span class="string">SELECT_ARGS=&quot;&quot;</span>    <span class="comment"># manifest が無ければ全量build</span></span><br><span class="line"><span class="string">fi</span></span><br></pre></td></tr></table></figure>

<p>初回はまだ基準が無いので全量 build が走り、その成功時に<strong>最初の manifest</strong> が GCS に置かれます。2回目以降はそれが基準として使われる、という流れです。</p>
<h2 id="7-動かすとどうなるか">7. 動かすとどうなるか</h2><p>仕組みと実装がそろいました。実際に PR を出すと、どう動くのかを見てみます。</p>
<p>たとえば <code>int_sales</code> を1つ変更した PR を出すと、<code>slim-ci.yml</code> の「Show selected models」ステップのログに、こんなふうに出ます。</p>
<figure class="highlight text"><table><tr><td class="code"><pre><span class="line">$ dbt ls --select state:modified+ --state prod-run-artifacts --target ci</span><br><span class="line">my_dbt_project.int_sales</span><br><span class="line">my_dbt_project.mart_sales_daily</span><br><span class="line">my_dbt_project.mart_sales_report</span><br></pre></td></tr></table></figure>

<p>変更した <code>int_sales</code> と、その下流の <code>mart_sales_daily</code>・<code>mart_sales_report</code> だけが選ばれています。上流の <code>stg_orders</code>・<code>stg_items</code> は選ばれず、run&#x2F;test の対象になりません（CI 環境に無い上流は <code>--defer</code> で本番のテーブルを参照します）。まさに2章で描いた変更の影響範囲の通りです。</p>
<p>そしてこの PR をマージすると <code>deploy.yml</code> が走り、本番に反映したあと最新 manifest が GCS に上がります。次に誰かが PR を出すときには、その新しい manifest が基準になっている——これで一巡です。</p>
<p>普段なにげなく PR を出すと CI が回っていますが、その裏では毎回この「基準を取ってきて、差分を測って、変更分だけ検証する」が動いている、というわけです。</p>
<h2 id="8-補足：テストは-PR-側で回す">8. 補足：テストは PR 側で回す</h2><p>最後に、テストをどこで回すかについて1つ補足します。</p>
<p>本記事のサンプルでは、テスト（unit test もデータテストも）は <strong>PR 側の Slim CI</strong> で、変更の影響範囲に対して実行しています（5章の <code>dbt test --select state:modified+ ...</code>）。一方 <code>deploy.yml</code> はデプロイ（run）に絞っています。とくに unit test（モデルのロジックを、固定した入力データで検証するテスト）は、コードのロジックを確かめるものなので、マージ前の PR 側で回すのが自然です。</p>
<p>なお、デプロイ<strong>後</strong>にもテストを回したくなる場面があります。たとえば「本番に反映されたデータそのものが、想定どおりの品質になっているか」を継続的にチェックしたい場合です。そのときは <code>deploy.yml</code> の <code>dbt run</code> を <code>dbt build</code>（run + test）に変えると、本番反映とあわせてデータテストも走らせられます。</p>
<p>ただしその場合、unit test は <code>--exclude test_type:unit</code> で外すのがおすすめです。unit test は固定の入力データでロジックを検証するテストなので、本番データに対して回しても意味がなく、余計な時間がかかるだけだからです。</p>
<h2 id="9-まとめ">9. まとめ</h2><p>本記事では、dbt Slim CI を GitHub Actions と GCS でどう実現するかを、仕組みから見てきました。</p>
<ul>
<li>素朴な全量 build の CI は、プロジェクトが育つと<strong>時間・お金・ノイズ</strong>の3つの課題で効いてくる</li>
<li>解決策は「変更モデル + その下流」だけを検証し、変わっていない上流は本番を参照すること</li>
<li>それを dbt で実現するのが <code>state:modified+</code> &#x2F; <code>--state</code> &#x2F; <code>--defer</code> の3フラグ</li>
<li>そして Slim CI の心臓は、<strong>マージ成功時に基準 manifest を GCS へ最新化し、次の PR がそれを参照する</strong>という循環</li>
</ul>
<p>改めて振り返ると、Slim CI の本体は個々のフラグそのものではなく、<strong>「manifest（＝プロジェクトの設計図）を、いつ・どこで・誰が更新するか」という設計</strong>にあります。ここさえ腹落ちしていれば、認証方式やディレクトリ構成が違っても、自分の環境に合わせて組み立てられるはずです。</p>
<p>この記事が、Slim CI の仕組みを理解したり、導入の一歩を踏み出したりするきっかけになれば幸いです。</p>
]]></content>
    <summary type="html">dbt の CI を効率化する Slim CI を、dbt Core + BigQuery + GitHub Actions + Google Cloud Storage（以下 GCS） の構成で、どうやって実現するのかを解説します。</summary>
    <category term="DataEngineering" scheme="https://future-architect.github.io/categories/DataEngineering/"/>
    <category term="GCS" scheme="https://future-architect.github.io/tags/GCS/"/>
    <category term="GitHubActions" scheme="https://future-architect.github.io/tags/GitHubActions/"/>
    <category term="GoogleCloud" scheme="https://future-architect.github.io/tags/GoogleCloud/"/>
    <category term="dbt" scheme="https://future-architect.github.io/tags/dbt/"/>
  </entry>
  <entry>
    <title>S3 TablesのIceberg形式で、timestamptz型の登録に苦戦した話</title>
    <link href="https://future-architect.github.io/articles/20260702a/"/>
    <id>https://future-architect.github.io/articles/20260702a/</id>
    <published>2026-07-01T15:00:00.000Z</published>
    <updated>2026-07-01T15:00:00.000Z</updated>
    <author><name>鈴木風真</name></author>
    <content type="html"><![CDATA[<p>データエンジニアリング連載の2本目です。</p>
<h2 id="はじめに">はじめに</h2><p>こんにちは、フューチャーの鈴木風真です！</p>
<p>S3 Tables(Iceberg形式)にはtimestamptz(タイムゾーンつきタイムスタンプ)という型があります。今回、このtimestamptz型でカラムを登録しようとしたとき、いろいろ面白い発見があったので、記事にしたいと思います。</p>
<h2 id="timestamptzとは">timestamptzとは</h2><p>timestamptzはApache Icebergにおける型の種類の1つで、タイムゾーン付きタイムスタンプと呼ばれます。その名の通り、「日本時間」などのタイムゾーン情報とともに時刻を保持できる型です。時差に左右されない世界中の「まさにあの瞬間！」という絶対的なタイミングを記録できる便利なデータ型です。</p>
<p>日本時間で書き込んでも別の国の時間で書き込んでも、内部的にはすべてUTC（協定世界時）に揃えて保存してくれるため、あとから時間を計算し直すような面倒な手間がかかりません。</p>
<h2 id="Athenaから登録することはできない！？">Athenaから登録することはできない！？</h2><p>Athenaからtimestamptz型を含むCREATE文を書いてS3 Tablesを登録してみましょう。するとこんなエラーがでます。</p>
<img fetchpriority="high" src="/images/2026/20260702a/image.png" alt="image.png" width="1108" height="631">

<p>timestamp with timezoneに変えて実行してもうまくいきません。AWS公式サイトを確認してみると、以下の文言が。。。</p>
<blockquote>
<p>CREATE TABLE などの Athena Iceberg DDL ステートメントでサポートされているのは、Iceberg タイムスタンプ (タイムゾーンなし) のみですが、Athena を介してすべてのタイムスタンプ型をクエリできます。<br>Athena の Iceberg テーブルでサポートされているデータ型</p>
</blockquote>
<p>つまり、AthenaではtimestamptzのSELECTはできるがCREATEはできないということです。(なんじゃそれ！)この時点で、AthenaでDDLを実行してテーブルを作成する選択肢はなくなりました。</p>
<p>調べてみると、AWS Glue経由でDDLを実行すると、timetamptz型も登録できるようです。DDL実行用のGlueジョブを作成するのは少々面倒でしたが、Glueを使えば、<strong>DDLファイルの配置→配置をトリガーにDDLを実行</strong> みたいなパイプラインを作るのもやりやすいかなと思い、Glueでやることにしました。</p>
<h2 id="Glue経由でDDLを実行">Glue経由でDDLを実行</h2><p>というわけで、簡単なDDL実行用のGlueジョブを作ってみました。</p>
<p>DDLファイルには複数のCREATE文を含む想定で、Glueジョブ側でステートメントごとにループ処理させるようにしました。既存のテーブルを登録しようとするとエラーになりますが、かといって<code>DROP TABLE IF EXISTS</code>みたいなことをやって事故るのは怖かったので、あえてそのままエラーになるようにしています。結果として、100行程度のシンプルなGlueジョブになりました。</p>
<figure class="highlight py"><table><tr><td class="code"><pre><span class="line"><span class="comment"># ... (boto3やGlue、Spark関連の標準的なimport文は省略) ...</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 接続先のカタログ名・ネームスペース名・バケットARN</span></span><br><span class="line">S3TABLES_CATALOG = <span class="string">&quot;s3tables&quot;</span></span><br><span class="line">S3TABLES_NAMESPACE = <span class="string">&quot;default&quot;</span></span><br><span class="line">S3TABLES_BUCKET_ARN = <span class="string">&quot;&lt;your-bucket-arn&gt;&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">main</span>() -&gt; <span class="literal">None</span>:</span><br><span class="line">    <span class="comment"># ... (ジョブパラメータ取得・Glue/Sparkセッションの初期化処理は省略) ...</span></span><br><span class="line">    <span class="comment"># ※ ここでは変数 spark (SparkSession) と args が利用可能な状態とします</span></span><br><span class="line"></span><br><span class="line">    <span class="comment"># S3 Tables用のIcebergカタログをSparkに登録する</span></span><br><span class="line">    spark.conf.<span class="built_in">set</span>(</span><br><span class="line">        <span class="string">f&quot;spark.sql.catalog.<span class="subst">&#123;S3TABLES_CATALOG&#125;</span>&quot;</span>,</span><br><span class="line">        <span class="string">&quot;org.apache.iceberg.spark.SparkCatalog&quot;</span>,</span><br><span class="line">    )</span><br><span class="line">    spark.conf.<span class="built_in">set</span>(</span><br><span class="line">        <span class="string">f&quot;spark.sql.catalog.<span class="subst">&#123;S3TABLES_CATALOG&#125;</span>.catalog-impl&quot;</span>,</span><br><span class="line">        <span class="string">&quot;software.amazon.s3tables.iceberg.S3TablesCatalog&quot;</span>,</span><br><span class="line">    )</span><br><span class="line">    spark.conf.<span class="built_in">set</span>(</span><br><span class="line">        <span class="string">f&quot;spark.sql.catalog.<span class="subst">&#123;S3TABLES_CATALOG&#125;</span>.warehouse&quot;</span>,</span><br><span class="line">        S3TABLES_BUCKET_ARN,</span><br><span class="line">    )</span><br><span class="line"></span><br><span class="line">    <span class="comment"># テーブルの親となるネームスペースを作成する（未作成の場合のみ）</span></span><br><span class="line">    spark.sql(</span><br><span class="line">        <span class="string">f&quot;CREATE NAMESPACE IF NOT EXISTS <span class="subst">&#123;S3TABLES_CATALOG&#125;</span>.<span class="subst">&#123;S3TABLES_NAMESPACE&#125;</span>&quot;</span></span><br><span class="line">    )</span><br><span class="line"></span><br><span class="line">    <span class="comment"># S3からDDLファイルをテキストとして読み込む</span></span><br><span class="line">    <span class="comment"># ※ read_s3_file: boto3による単純なS3オブジェクト読み込み処理のため実装は省略</span></span><br><span class="line">    ddl_content = read_s3_file(args[<span class="string">&quot;ddl_file_path&quot;</span>])</span><br><span class="line"></span><br><span class="line">    <span class="comment"># 読み込んだDDLをセミコロンで分割し、実行可能なステートメントのリストに変換</span></span><br><span class="line">    statements = parse_ddl(ddl_content)</span><br><span class="line">    <span class="built_in">print</span>(<span class="string">f&quot;[INFO] <span class="subst">&#123;<span class="built_in">len</span>(statements)&#125;</span> 件のDDL文を実行します。&quot;</span>)</span><br><span class="line"></span><br><span class="line">    <span class="comment"># ★ここがポイント: 分割したDDL文をループ処理で1つずつSparkSQLで実行する</span></span><br><span class="line">    <span class="keyword">for</span> i, stmt <span class="keyword">in</span> <span class="built_in">enumerate</span>(statements, start=<span class="number">1</span>):</span><br><span class="line">        spark.sql(stmt)</span><br><span class="line">        <span class="built_in">print</span>(<span class="string">f&quot;[INFO] DDL (<span class="subst">&#123;i&#125;</span>/<span class="subst">&#123;<span class="built_in">len</span>(statements)&#125;</span>) 完了&quot;</span>)</span><br><span class="line"></span><br><span class="line">    <span class="built_in">print</span>(<span class="string">&quot;[INFO] 全DDL実行完了。&quot;</span>)</span><br><span class="line">    job.commit()</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">parse_ddl</span>(<span class="params">ddl_content: <span class="built_in">str</span></span>) -&gt; <span class="built_in">list</span>[<span class="built_in">str</span>]:</span><br><span class="line">    <span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">    DDLテキストをセミコロンで分割し、空行やコメントのみの断片を除外して返す関数</span></span><br><span class="line"><span class="string">    &quot;&quot;&quot;</span></span><br><span class="line">    <span class="keyword">return</span> [</span><br><span class="line">        stmt.strip()</span><br><span class="line">        <span class="keyword">for</span> stmt <span class="keyword">in</span> ddl_content.split(<span class="string">&quot;;&quot;</span>)</span><br><span class="line">        <span class="keyword">if</span> <span class="built_in">any</span>(</span><br><span class="line">            line.strip() <span class="keyword">and</span> <span class="keyword">not</span> line.strip().startswith(<span class="string">&quot;--&quot;</span>)</span><br><span class="line">            <span class="keyword">for</span> line <span class="keyword">in</span> stmt.strip().splitlines()</span><br><span class="line">        )</span><br><span class="line">    ]</span><br><span class="line"></span><br><span class="line"><span class="comment"># ... (if __name__ == &quot;__main__&quot;: などの呼び出し部は省略) ...</span></span><br></pre></td></tr></table></figure>

<p>次にDDLファイルを用意します。その前に、S3 Tables(Iceberg)と、Glue(Spark SQL)、AthenaのSQLにおけるtimestamp型の対応を確認します。</p>
<h3 id="Icebergのタイムスタンプ型対応表">Icebergのタイムスタンプ型対応表</h3><div class="scroll"><table>
<thead>
<tr>
<th align="left">Icebergの型</th>
<th align="left">説明</th>
<th align="left">Spark SQLのDDL</th>
<th align="left">AthenaのDDL</th>
</tr>
</thead>
<tbody><tr>
<td align="left"><strong><code>timestamptz</code></strong></td>
<td align="left">タイムゾーンつきタイムスタンプ</td>
<td align="left"><strong><code>TIMESTAMP</code></strong></td>
<td align="left"><strong>定義不可</strong><br>（※Spark等で作成する必要あり）</td>
</tr>
<tr>
<td align="left"><strong><code>timestamp</code></strong></td>
<td align="left">タイムゾーンなしタイムスタンプ</td>
<td align="left"><strong><code>TIMESTAMP_NTZ</code></strong></td>
<td align="left"><strong><code>TIMESTAMP</code></strong></td>
</tr>
</tbody></table></div>
<blockquote>
<p>Apache Spark公式サイトを参考に作成</p>
</blockquote>
<p>表で書くと分かりやすいですが、ここに大きなトラップがありますね！<br>つまり、</p>
<ul>
<li>Spark SQLで TIMESTAMP と書く → timestamptz（タイムゾーンあり）になる</li>
<li>Athenaで TIMESTAMP と書く → timestamp（タイムゾーンなし）になる</li>
</ul>
<p>という風に、<strong>同じTIMESTAMPと指定しても真逆になる</strong>ということです。</p>
<p>したがって、Spark SQLだとtimestamptzに相当するのが「TIMESTAMP」になるので、作成するSQLファイルでは単にTIMESTAMPと定義しておきます。</p>
<p>※当然「timestamptz」と定義すると「そんな型はない！」とエラーになります。</p>
<figure class="highlight sql"><table><tr><td class="code"><pre><span class="line"><span class="keyword">CREATE TABLE</span> event_time (</span><br><span class="line">  event_time <span class="type">timestamp</span> COMMENT <span class="string">&#x27;イベント日時&#x27;</span></span><br><span class="line">)</span><br><span class="line"><span class="keyword">USING</span> iceberg</span><br><span class="line">COMMENT <span class="string">&#x27;イベント日次テーブル&#x27;</span></span><br><span class="line">TBLPROPERTIES (</span><br><span class="line">  <span class="string">&#x27;format-version&#x27;</span><span class="operator">=</span><span class="string">&#x27;2&#x27;</span>,</span><br><span class="line">  <span class="string">&#x27;write_compression&#x27;</span><span class="operator">=</span><span class="string">&#x27;zstd&#x27;</span></span><br><span class="line">);</span><br></pre></td></tr></table></figure>

<p>このDDLファイルをS3に配置します。Glue側でSQLファイルパスをパラメータで指定してあげると、それをGlueが見に行って実行してくれるようにしました。</p>
<img src="/images/2026/20260702a/image_2.png" alt="image.png" width="811" height="529" loading="lazy">

<p>試しに実行してみます。すると1分ほどで成功します。簡単な処理でもGlueって結構遅いんですよね。</p>
<img src="/images/2026/20260702a/image_3.png" alt="image.png" width="1104" height="671" loading="lazy">

<p>実際にテーブルが作成されているかを見てみましょう。S3の画面から作成されたテーブルを見てみます。</p>
<img src="/images/2026/20260702a/image_4.png" alt="image.png" width="1200" height="287" loading="lazy">

<p>たしかに、テーブルが作成されていることを確認できます。しかしこれだと、型が確認できないので肝心のtimestamptz型が登録されているかが分かりません。そこで、Athenaから登録されたテーブルを見てみます。Athenaからはエディタの画面からテーブルのデータ型が確認できます。</p>
<img src="/images/2026/20260702a/image_5.png" alt="image.png" width="366" height="502" loading="lazy">

<p>よしよし、ってあれっ？timestamptzになってない！</p>
<h2 id="Athenaからtimestamptzは確認できない！？">Athenaからtimestamptzは確認できない！？</h2><p>どういうわけか、わざわざGlue経由でDDLを実行したのに、timestamptzで登録されていないです。しかし、SparkにおけるtimestampはIcebergだとtimestamptzになるはず、、もしかして、Athena画面上の表示の問題か？ …というわけで、テーブル定義そのものを覗いてみることにしました。テーブルの実体は、S3のテーブルバケットから確認できるように、メタデータで定義されています。</p>
<img src="/images/2026/20260702a/40600451-1f33-483c-885e-c4340cedc2c9.png" alt="40600451-1f33-483c-885e-c4340cedc2c9.png" width="1200" height="510" loading="lazy">

<p>このファイルはダウンロードしてみることはできないので、AWSコマンドを叩いて覗くしかないです。以下コマンドを実行してみます。</p>
<figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">aws s3 <span class="built_in">cp</span> [テーブルメタデータARN] - | jq .</span><br></pre></td></tr></table></figure>

<p>(jsonファイルの中身を取得し、コマンドライン上で見やすく表示させる簡単なコマンドです)<br>すると、こんな結果が返ってきます。</p>
<figure class="highlight json"><table><tr><td class="code"><pre><span class="line"><span class="attr">&quot;schemas&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">    <span class="punctuation">&#123;</span></span><br><span class="line">      <span class="attr">&quot;type&quot;</span><span class="punctuation">:</span> <span class="string">&quot;struct&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;schema-id&quot;</span><span class="punctuation">:</span> <span class="number">0</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;fields&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">        <span class="punctuation">&#123;</span></span><br><span class="line">          <span class="attr">&quot;id&quot;</span><span class="punctuation">:</span> <span class="number">1</span><span class="punctuation">,</span></span><br><span class="line">          <span class="attr">&quot;name&quot;</span><span class="punctuation">:</span> <span class="string">&quot;event_time&quot;</span><span class="punctuation">,</span></span><br><span class="line">          <span class="attr">&quot;required&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">false</span></span><span class="punctuation">,</span></span><br><span class="line">          <span class="attr">&quot;type&quot;</span><span class="punctuation">:</span> <span class="string">&quot;timestamptz&quot;</span><span class="punctuation">,</span></span><br><span class="line">          <span class="attr">&quot;doc&quot;</span><span class="punctuation">:</span> <span class="string">&quot;イベント日時&quot;</span></span><br><span class="line">        <span class="punctuation">&#125;</span></span><br><span class="line">      <span class="punctuation">]</span></span><br><span class="line">    <span class="punctuation">&#125;</span></span><br><span class="line">  <span class="punctuation">]</span><span class="punctuation">,</span></span><br></pre></td></tr></table></figure>

<p>いや、ちゃんとtimetamptzで登録されてる笑</p>
<p>逆にAthenaで登録したテーブルはしっかりタイムゾーン無しtimestamp型で登録されていることも確認できます。つまり、<strong>Athenaではtimestamp型もtimestamptz型も、画面上では書き分けずに表示する仕様</strong>っぽいです！ややこしいですね！</p>
<p>S3 Tablesの型定義がS3の画面上から確認できないなど、S3 Tablesは新しい技術だからか、まだ整備されていない感があるなと思いました。</p>
<h2 id="おわりに～AI時代の技術ブログについて～">おわりに～AI時代の技術ブログについて～</h2><p>この記事では、試行錯誤する中での驚きだったり笑いなどの<strong>感情</strong>を乗せて書くことを意識しました。</p>
<p>AIで答えがすぐに得られる時代に、技術ブログに求められることは何だろうかと考えたとき、それは<strong>ファクトと人間味</strong>なのではないかと思います。AIはハルシネーションも起こすし、結局のところ「言っているだけ」です。だからこそ、「実際にやってみた」というファクトが価値を持つと思います。その次に人間っぽい感想。筆者が何を考え、感じたのか。一読者としてはそれが見たいです。</p>
<p>今回の記事も、せんじ詰めれば「timestamptzはAthenaからだと登録できないが、Glue経由なら登録が可能。型の確認はメタデータを直接見る必要がある」というだけです。でも、それだけだと面白くない。その答えにたどり着くまでの、推理・発見・驚き・笑いといった、人間味のある試行錯誤の過程が、一読者としては読みたいです。</p>
<p>自分含め、トラブルシュートで「ググる」人はどんどん減っている気がします。それに伴いあらゆるWebサイトは徐々に見られなくなっている。そんな時代でも「フューチャーの技術ブログは読みたい」と思ってもらえる記事を出していきたいなと思っています。最後まで読んでいただきありがとうございました！</p>
]]></content>
    <summary type="html">S3 Tablesという型があります。今回、このtimestamptz型でカラムを登録しようとしたとき、いろいろ面白い発見があったので、記事にしたいと思います。</summary>
    <category term="DataEngineering" scheme="https://future-architect.github.io/categories/DataEngineering/"/>
    <category term="AWS" scheme="https://future-architect.github.io/tags/AWS/"/>
    <category term="S3" scheme="https://future-architect.github.io/tags/S3/"/>
  </entry>
  <entry>
    <title>Workspace Events API でGWSフォルダの変更をリアルタイム検知し、スプレッドシートを BigQuery に自動取り込みする</title>
    <link href="https://future-architect.github.io/articles/20260630a/"/>
    <id>https://future-architect.github.io/articles/20260630a/</id>
    <published>2026-06-29T15:00:00.000Z</published>
    <updated>2026-06-29T15:00:00.000Z</updated>
    <author><name>柴田健太</name></author>
    <content type="html"><![CDATA[<img fetchpriority="high" src="/images/2026/20260630a/top.jpg" alt="" width="720" height="393">

<p>こんにちは。フューチャーアーキテクト 製造・エネルギーサービス事業部の柴田です😌 見真の心で本質を探求しています!</p>
<p>2026年初めての試みとして、<strong>データエンジニアリング</strong>をテーマにしたブログ連載を開催します🎉</p>
<p>この記事は1本目です。</p>
<div class="scroll"><table>
<thead>
<tr>
<th align="left">日付</th>
<th align="left">執筆者</th>
<th align="left">タイトル &#x2F; テーマ</th>
</tr>
</thead>
<tbody><tr>
<td align="left">6&#x2F;30（火）</td>
<td align="left">柴田健太さん</td>
<td align="left">Workspace Events API で GWS フォルダの変更をリアルタイム検知し、スプレッドシートを BigQuery に自動取り込みする（この記事です）</td>
</tr>
<tr>
<td align="left">7&#x2F;1（水）</td>
<td align="left">真野隼記さん</td>
<td align="left">データガバナンス設計ガイドラインを公開しました</td>
</tr>
<tr>
<td align="left">7&#x2F;2（木）</td>
<td align="left">鈴木風真さん</td>
<td align="left">S3 TablesのIceberg形式で、timestamptz型の登録に苦戦した話</td>
</tr>
<tr>
<td align="left">7&#x2F;3（金）</td>
<td align="left">棚井龍之介さん</td>
<td align="left">主要AIモデルのデータ保持期間とZDRを、一次情報で確認する</td>
</tr>
<tr>
<td align="left">7&#x2F;6（月）</td>
<td align="left">片岡久人さん</td>
<td align="left">仕組みからわかる dbt Slim CI — GitHub Actions × Google Cloud Storage で実現する差分実行</td>
</tr>
</tbody></table></div>
<h2 id="はじめに">はじめに</h2><p>「Google Drive の共有フォルダにあるスプレッドシートが更新されたら、自動的にテーブルとしてBigQueryに取り込みたい」。このような仕組みを作りたいと思うことは誰にでもあると思います。</p>
<p>従来は Google Drive API の <code>changes.watch</code> を使った Webhook ベースの方法が一般的でしたが、Webhookサーバーの運用が必要で、やりたいことのシンプルさに対してとても手間がかかっていました。</p>
<p>2025年7月に Google が <strong>Google Workspace Events API</strong> をアップデートし、Drive フォルダの変更を直接 Cloud Pub&#x2F;Sub に通知できるようになりました（2026年6月時点でDeveloper Public Preview）。これにより、リアルタイムなデータパイプライン構築が格段に容易になりました。</p>
<p>本記事では、以下の最小構成でスプレッドシートの自動取り込みパイプラインを構築する手順を解説します。</p>
<h3 id="アーキテクチャ">アーキテクチャ</h3><figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">Google Drive フォルダ</span><br><span class="line">    │  ファイル変更（作成・編集・削除）</span><br><span class="line">    ▼</span><br><span class="line">Workspace Events API（プレビュー）</span><br><span class="line">    │  CloudEvent 形式でイベント発行</span><br><span class="line">    ▼</span><br><span class="line">Cloud Pub/Sub Topic</span><br><span class="line">    │  メッセージ受信でトリガー</span><br><span class="line">    ▼</span><br><span class="line">Cloud Functions（Python 3.12）</span><br><span class="line">    │  ① イベント解析</span><br><span class="line">    │  ② Sheets API でスプレッドシート読み取り</span><br><span class="line">    │  ③ BigQuery に書き込み</span><br><span class="line">    ▼</span><br><span class="line">BigQuery</span><br><span class="line">    ├── table（1スプシに対して1テーブル作成）</span><br><span class="line">    └── ...</span><br></pre></td></tr></table></figure>

<h3 id="各コンポーネントの役割">各コンポーネントの役割</h3><div class="scroll"><table>
<thead>
<tr>
<th>#</th>
<th>コンポーネント</th>
<th>サービス</th>
<th>役割</th>
<th>補足</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>変更検知</td>
<td>Workspace Events API</td>
<td>Drive フォルダ内のファイル変更（作成・編集・削除）を検知し、イベントを発行する</td>
<td>Developer Public Preview<br><strong>サブスクリプションの有効期限は最大7日間</strong></td>
</tr>
<tr>
<td>2</td>
<td>イベント中継</td>
<td>Cloud Pub&#x2F;Sub</td>
<td>イベントメッセージを受信・保持し、後続の処理にリアルタイムで配信する</td>
<td>at-least-once 配信。メッセージの順序保証なし</td>
</tr>
<tr>
<td>3</td>
<td>データ処理</td>
<td>Cloud Functions</td>
<td>Pub&#x2F;Sub のメッセージをトリガーに起動し、スプレッドシートの読み取り・BigQuery への書き込みを行う</td>
<td>今回はスプレッドシート以外のファイルは自動スキップ</td>
</tr>
<tr>
<td>4</td>
<td>データ読み取り</td>
<td>Google Sheets API</td>
<td>Cloud Functions から呼び出され、スプレッドシートのデータを取得する</td>
<td>ヘッダー行（1行目）をカラム名として使用</td>
</tr>
<tr>
<td>5</td>
<td>データ蓄積</td>
<td>BigQuery</td>
<td>1スプシに対して1テーブルでデータを格納する</td>
<td>ファイル名やヘッダー行そのままテーブル名とカラム名として扱う</td>
</tr>
</tbody></table></div>
<h3 id="この構成の特徴">この構成の特徴</h3><div class="scroll"><table>
<thead>
<tr>
<th>項目</th>
<th>説明</th>
</tr>
</thead>
<tbody><tr>
<td>サーバーレス</td>
<td>Cloud Functions + Pub&#x2F;Sub で運用不要</td>
</tr>
<tr>
<td>リアルタイム</td>
<td>ファイル変更から数秒〜数十秒で BigQuery に反映</td>
</tr>
<tr>
<td>最小コスト</td>
<td>従量課金のみ、アイドル時はゼロコスト</td>
</tr>
<tr>
<td>Webhook 不要</td>
<td>Workspace Events API が直接 Pub&#x2F;Sub に通知</td>
</tr>
</tbody></table></div>
<h2 id="前提条件">前提条件</h2><ul>
<li>Google Cloud プロジェクトを利用</li>
<li>Google Workspace アカウント（ビジネス以上）</li>
<li>Python 3.12+</li>
</ul>
<h2 id="手順1-Google-Cloud-インフラ構築">手順1: Google Cloud インフラ構築</h2><p>この手順はGoogle CloudのCloud Shellで実行します。</p>
<h3 id="1-1-環境変数の設定">1-1. 環境変数の設定</h3><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="built_in">export</span> GCP_PROJECT_ID=<span class="string">&quot;your-project-id&quot;</span></span><br><span class="line"><span class="built_in">export</span> DRIVE_FOLDER_ID=<span class="string">&quot;your-drive-folder-id&quot;</span></span><br></pre></td></tr></table></figure>

<blockquote>
<p><strong>フォルダ ID の確認方法</strong><br>Google Drive でフォルダを開き、URL の末尾にある文字列がフォルダ ID です。<br><code>https://drive.google.com/drive/folders/XXXXXXXXXX</code> ← この <code>XXXXXXXXXX</code> 部分</p>
</blockquote>
<h3 id="1-2-API-の有効化">1-2. API の有効化</h3><p>今回は以下のAPIを有効化しています。</p>
<p>【Google Driveの変更通知のために必要】</p>
<ul>
<li>Cloud Pub&#x2F;Sub API (pubsub.googleapis.com)</li>
<li>Google Workspace Events API(workspaceevents.googleapis.com)</li>
</ul>
<p>【スプシテーブル化処理のために必要】</p>
<ul>
<li>Google Drive API (drive.googleapis.com)</li>
<li>Google Sheets API (sheets.googleapis.com)</li>
<li>Cloud Functions API (cloudfunctions.googleapis.com)</li>
<li>Cloud Build API (cloudbuild.googleapis.com)</li>
<li>BigQuery API (bigquery.googleapis.com)</li>
</ul>
<h3 id="1-3-Pub-Sub-Topic-の作成">1-3. Pub&#x2F;Sub Topic の作成</h3><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="comment"># Topic 作成</span></span><br><span class="line">gcloud pubsub topics create drive-events-topic \</span><br><span class="line">  --project=<span class="string">&quot;<span class="variable">$&#123;GCP_PROJECT_ID&#125;</span>&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># Workspace Events API のサービスアカウントに Publisher 権限を付与</span></span><br><span class="line">gcloud pubsub topics add-iam-policy-binding drive-events-topic \</span><br><span class="line">  --member=<span class="string">&quot;serviceAccount:workspace-subs-prod-gcp-sa@gcp-sa-workspace-events.iam.gserviceaccount.com&quot;</span> \</span><br><span class="line">  --role=<span class="string">&quot;roles/pubsub.publisher&quot;</span> \</span><br><span class="line">  --project=<span class="string">&quot;<span class="variable">$&#123;GCP_PROJECT_ID&#125;</span>&quot;</span></span><br></pre></td></tr></table></figure>

<div class="note-container note-info"><span class="note-icon"></span><div>

<p>補足<br><code>workspace-subs-prod-gcp-sa@gcp-sa-workspace-events.iam.gserviceaccount.com</code> は Google が管理する固定のサービスアカウントです。この権限がないとイベントが Pub&#x2F;Sub に届きません。</p>
</div></div>

<h3 id="1-4-BigQuery-データセットの作成">1-4. BigQuery データセットの作成</h3><p>今回はスプシをテーブル化してBigQueryに取り込みますので、テーブルのデータセットだけあらかじめ作成します。</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="comment"># データセット作成</span></span><br><span class="line">bq mk --dataset \</span><br><span class="line">  --location=asia-northeast1 \</span><br><span class="line">  --description=<span class="string">&quot;自動スプシ取り込み先のデータセット&quot;</span> \</span><br><span class="line">  <span class="variable">$&#123;GCP_PROJECT_ID&#125;</span>:gws_drive_events</span><br></pre></td></tr></table></figure>

<h2 id="手順2-Cloud-Function-の実装">手順2: Cloud Function の実装</h2><h3 id="2-1-ディレクトリ構成">2-1. ディレクトリ構成</h3><figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">cloud_function/</span><br><span class="line">├── main.py           <span class="comment"># メインコード</span></span><br><span class="line">└── requirements.txt  <span class="comment"># 依存パッケージ</span></span><br></pre></td></tr></table></figure>

<h3 id="2-2-ソースコード">2-2. ソースコード</h3><details><summary>requirements.txt</summary>

<figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">functions-framework==3.*</span><br><span class="line">google-cloud-bigquery==3.*</span><br><span class="line">google-api-python-client==2.*</span><br><span class="line">google-auth==2.*</span><br></pre></td></tr></table></figure>

</details>

<details><summary>main.py</summary>

<figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> base64</span><br><span class="line"><span class="keyword">import</span> json</span><br><span class="line"><span class="keyword">import</span> os</span><br><span class="line"><span class="keyword">import</span> re</span><br><span class="line"><span class="keyword">import</span> traceback</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> functions_framework</span><br><span class="line"><span class="keyword">from</span> cloudevents.http <span class="keyword">import</span> CloudEvent</span><br><span class="line"><span class="keyword">from</span> google.cloud <span class="keyword">import</span> bigquery</span><br><span class="line"><span class="keyword">from</span> google.oauth2.credentials <span class="keyword">import</span> Credentials</span><br><span class="line"><span class="keyword">from</span> googleapiclient.discovery <span class="keyword">import</span> build</span><br><span class="line"></span><br><span class="line">PROJECT_ID = os.environ.get(<span class="string">&quot;PROJECT_ID&quot;</span>, <span class="string">&quot;your-project-id&quot;</span>)</span><br><span class="line">DATASET_ID = os.environ.get(<span class="string">&quot;DATASET_ID&quot;</span>, <span class="string">&quot;gws_drive_events&quot;</span>)</span><br><span class="line">TABLE_SPREADSHEET_DATA = os.environ.get(<span class="string">&quot;TABLE_SPREADSHEET_DATA&quot;</span>, <span class="string">&quot;spreadsheet_data&quot;</span>)</span><br><span class="line">SPREADSHEET_MIME_TYPE = <span class="string">&quot;application/vnd.google-apps.spreadsheet&quot;</span></span><br><span class="line"></span><br><span class="line">SCOPES = [<span class="string">&quot;https://www.googleapis.com/auth/drive.readonly&quot;</span>, <span class="string">&quot;https://www.googleapis.com/auth/spreadsheets.readonly&quot;</span>]</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">extract_file_id</span>(<span class="params">cloud_event: CloudEvent, event_data: <span class="built_in">dict</span></span>) -&gt; <span class="built_in">str</span>:</span><br><span class="line">    <span class="string">&quot;&quot;&quot;Workspace Events / Pub/Sub の複数フォーマットから file_id を抽出する。&quot;&quot;&quot;</span></span><br><span class="line">    candidates = [</span><br><span class="line">        cloud_event.get(<span class="string">&quot;subject&quot;</span>, <span class="string">&quot;&quot;</span>),</span><br><span class="line">        event_data.get(<span class="string">&quot;subject&quot;</span>, <span class="string">&quot;&quot;</span>),</span><br><span class="line">        event_data.get(<span class="string">&quot;resource&quot;</span>, &#123;&#125;).get(<span class="string">&quot;name&quot;</span>, <span class="string">&quot;&quot;</span>),</span><br><span class="line">        event_data.get(<span class="string">&quot;data&quot;</span>, &#123;&#125;).get(<span class="string">&quot;name&quot;</span>, <span class="string">&quot;&quot;</span>),</span><br><span class="line">        event_data.get(<span class="string">&quot;data&quot;</span>, &#123;&#125;).get(<span class="string">&quot;file&quot;</span>, &#123;&#125;).get(<span class="string">&quot;name&quot;</span>, <span class="string">&quot;&quot;</span>),</span><br><span class="line">    ]</span><br><span class="line"></span><br><span class="line">    attributes = cloud_event.data.get(<span class="string">&quot;message&quot;</span>, &#123;&#125;).get(<span class="string">&quot;attributes&quot;</span>, &#123;&#125;)</span><br><span class="line">    <span class="keyword">if</span> <span class="built_in">isinstance</span>(attributes, <span class="built_in">dict</span>):</span><br><span class="line">        candidates.append(attributes.get(<span class="string">&quot;ce-subject&quot;</span>, <span class="string">&quot;&quot;</span>))</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> value <span class="keyword">in</span> candidates:</span><br><span class="line">        <span class="keyword">if</span> <span class="keyword">not</span> value:</span><br><span class="line">            <span class="keyword">continue</span></span><br><span class="line">        <span class="keyword">match</span> = re.search(<span class="string">r&quot;files/([A-Za-z0-9_-]+)&quot;</span>, value)</span><br><span class="line">        <span class="keyword">if</span> <span class="keyword">match</span>:</span><br><span class="line">            <span class="keyword">return</span> <span class="keyword">match</span>.group(<span class="number">1</span>)</span><br><span class="line">    <span class="keyword">return</span> <span class="string">&quot;&quot;</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">read_spreadsheet</span>(<span class="params">sheets_service, file_id: <span class="built_in">str</span></span>) -&gt; <span class="built_in">list</span>[<span class="built_in">dict</span>]:</span><br><span class="line">    spreadsheet = sheets_service.spreadsheets().get(spreadsheetId=file_id).execute()</span><br><span class="line">    sheets_data = []</span><br><span class="line">    <span class="keyword">for</span> sheet <span class="keyword">in</span> spreadsheet.get(<span class="string">&quot;sheets&quot;</span>, []):</span><br><span class="line">        sheet_title = sheet[<span class="string">&quot;properties&quot;</span>][<span class="string">&quot;title&quot;</span>]</span><br><span class="line">        result = sheets_service.spreadsheets().values().get(spreadsheetId=file_id, <span class="built_in">range</span>=sheet_title).execute()</span><br><span class="line">        values = result.get(<span class="string">&quot;values&quot;</span>, [])</span><br><span class="line">        <span class="keyword">if</span> <span class="keyword">not</span> values:</span><br><span class="line">            <span class="keyword">continue</span></span><br><span class="line">        headers = values[<span class="number">0</span>]</span><br><span class="line">        rows = values[<span class="number">1</span>:]</span><br><span class="line">        sheets_data.append(&#123;<span class="string">&quot;sheet_name&quot;</span>: sheet_title, <span class="string">&quot;headers&quot;</span>: headers, <span class="string">&quot;rows&quot;</span>: rows&#125;)</span><br><span class="line">        <span class="comment"># 取り込み対象は1シート目のみ</span></span><br><span class="line">        <span class="keyword">break</span></span><br><span class="line">    <span class="keyword">return</span> sheets_data</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">is_english_filename</span>(<span class="params">file_name: <span class="built_in">str</span></span>) -&gt; <span class="built_in">bool</span>:</span><br><span class="line">    <span class="keyword">return</span> <span class="built_in">bool</span>(re.fullmatch(<span class="string">r&quot;[A-Za-z0-9 _.-]+&quot;</span>, file_name <span class="keyword">or</span> <span class="string">&quot;&quot;</span>))</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">sanitize_table_id</span>(<span class="params">file_name: <span class="built_in">str</span></span>) -&gt; <span class="built_in">str</span>:</span><br><span class="line">    base_name = re.sub(<span class="string">r&quot;\.[^.]+$&quot;</span>, <span class="string">&quot;&quot;</span>, file_name.strip())</span><br><span class="line">    table_id = re.sub(<span class="string">r&quot;[^A-Za-z0-9_]&quot;</span>, <span class="string">&quot;_&quot;</span>, base_name)</span><br><span class="line">    table_id = re.sub(<span class="string">r&quot;_+&quot;</span>, <span class="string">&quot;_&quot;</span>, table_id).strip(<span class="string">&quot;_&quot;</span>).lower()</span><br><span class="line">    <span class="keyword">if</span> <span class="keyword">not</span> table_id:</span><br><span class="line">        table_id = <span class="string">&quot;sheet_data&quot;</span></span><br><span class="line">    <span class="keyword">if</span> <span class="keyword">not</span> re.<span class="keyword">match</span>(<span class="string">r&quot;^[A-Za-z_]&quot;</span>, table_id):</span><br><span class="line">        table_id = <span class="string">f&quot;t_<span class="subst">&#123;table_id&#125;</span>&quot;</span></span><br><span class="line">    <span class="keyword">return</span> table_id[:<span class="number">128</span>]</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">normalize_headers</span>(<span class="params">headers: <span class="built_in">list</span>[<span class="built_in">str</span>]</span>) -&gt; <span class="built_in">list</span>[<span class="built_in">str</span>]:</span><br><span class="line">    normalized = []</span><br><span class="line">    used = &#123;&#125;</span><br><span class="line">    <span class="keyword">for</span> index, header <span class="keyword">in</span> <span class="built_in">enumerate</span>(headers, start=<span class="number">1</span>):</span><br><span class="line">        raw = (header <span class="keyword">or</span> <span class="string">&quot;&quot;</span>).strip() <span class="keyword">if</span> <span class="built_in">isinstance</span>(header, <span class="built_in">str</span>) <span class="keyword">else</span> <span class="built_in">str</span>(header)</span><br><span class="line">        <span class="comment"># 非英語ヘッダーの場合のみ col_n を使用</span></span><br><span class="line">        <span class="keyword">if</span> (<span class="keyword">not</span> raw) <span class="keyword">or</span> re.search(<span class="string">r&quot;[^\x00-\x7F]&quot;</span>, raw):</span><br><span class="line">            column = <span class="string">f&quot;col_<span class="subst">&#123;index&#125;</span>&quot;</span></span><br><span class="line">        <span class="keyword">else</span>:</span><br><span class="line">            column = re.sub(<span class="string">r&quot;[^A-Za-z0-9_]&quot;</span>, <span class="string">&quot;_&quot;</span>, raw)</span><br><span class="line">            column = re.sub(<span class="string">r&quot;_+&quot;</span>, <span class="string">&quot;_&quot;</span>, column).strip(<span class="string">&quot;_&quot;</span>)</span><br><span class="line">            <span class="keyword">if</span> <span class="keyword">not</span> column:</span><br><span class="line">                column = <span class="string">f&quot;col_<span class="subst">&#123;index&#125;</span>&quot;</span></span><br><span class="line">            <span class="keyword">elif</span> <span class="keyword">not</span> re.<span class="keyword">match</span>(<span class="string">r&quot;^[A-Za-z_]&quot;</span>, column):</span><br><span class="line">                column = <span class="string">f&quot;c_<span class="subst">&#123;column&#125;</span>&quot;</span></span><br><span class="line"></span><br><span class="line">        <span class="keyword">if</span> column <span class="keyword">in</span> used:</span><br><span class="line">            used[column] += <span class="number">1</span></span><br><span class="line">            column = <span class="string">f&quot;<span class="subst">&#123;column&#125;</span>_<span class="subst">&#123;used[column]&#125;</span>&quot;</span></span><br><span class="line">        <span class="keyword">else</span>:</span><br><span class="line">            used[column] = <span class="number">1</span></span><br><span class="line"></span><br><span class="line">        normalized.append(column[:<span class="number">128</span>])</span><br><span class="line">    <span class="keyword">return</span> normalized</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">ensure_table_exists</span>(<span class="params">bq_client, table_ref: <span class="built_in">str</span>, schema: <span class="built_in">list</span>[bigquery.SchemaField]</span>) -&gt; <span class="literal">None</span>:</span><br><span class="line">    <span class="keyword">try</span>:</span><br><span class="line">        table = bq_client.get_table(table_ref)</span><br><span class="line">        existing = &#123;field.name <span class="keyword">for</span> field <span class="keyword">in</span> table.schema&#125;</span><br><span class="line">        new_fields = [field <span class="keyword">for</span> field <span class="keyword">in</span> schema <span class="keyword">if</span> field.name <span class="keyword">not</span> <span class="keyword">in</span> existing]</span><br><span class="line">        <span class="keyword">if</span> new_fields:</span><br><span class="line">            table.schema = <span class="built_in">list</span>(table.schema) + new_fields</span><br><span class="line">            bq_client.update_table(table, [<span class="string">&quot;schema&quot;</span>])</span><br><span class="line">    <span class="keyword">except</span> Exception:</span><br><span class="line">        table = bigquery.Table(table_ref, schema=schema)</span><br><span class="line">        bq_client.create_table(table)</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">write_spreadsheet_data_by_file_table</span>(<span class="params"></span></span><br><span class="line"><span class="params">    bq_client, file_id: <span class="built_in">str</span>, file_name: <span class="built_in">str</span>, sheets_data: <span class="built_in">list</span>[<span class="built_in">dict</span>]</span></span><br><span class="line"><span class="params"></span>) -&gt; <span class="built_in">tuple</span>[<span class="built_in">str</span>, <span class="built_in">int</span>]:</span><br><span class="line">    table_id = sanitize_table_id(file_name)</span><br><span class="line">    table_ref = <span class="string">f&quot;<span class="subst">&#123;PROJECT_ID&#125;</span>.<span class="subst">&#123;DATASET_ID&#125;</span>.<span class="subst">&#123;table_id&#125;</span>&quot;</span></span><br><span class="line">    <span class="keyword">if</span> <span class="keyword">not</span> sheets_data:</span><br><span class="line">        <span class="keyword">return</span> table_ref, <span class="number">0</span></span><br><span class="line"></span><br><span class="line">    first_sheet = sheets_data[<span class="number">0</span>]</span><br><span class="line">    normalized_headers = normalize_headers(first_sheet[<span class="string">&quot;headers&quot;</span>])</span><br><span class="line">    schema = [bigquery.SchemaField(column, <span class="string">&quot;STRING&quot;</span>) <span class="keyword">for</span> column <span class="keyword">in</span> normalized_headers]</span><br><span class="line"></span><br><span class="line">    ensure_table_exists(bq_client, table_ref, schema)</span><br><span class="line">    bq_client.query(<span class="string">f&quot;TRUNCATE TABLE `<span class="subst">&#123;table_ref&#125;</span>`&quot;</span>).result()</span><br><span class="line"></span><br><span class="line">    rows = []</span><br><span class="line">    <span class="keyword">for</span> row_values <span class="keyword">in</span> first_sheet[<span class="string">&quot;rows&quot;</span>]:</span><br><span class="line">        row = &#123;&#125;</span><br><span class="line">        <span class="keyword">for</span> j, column_name <span class="keyword">in</span> <span class="built_in">enumerate</span>(normalized_headers):</span><br><span class="line">            row[column_name] = row_values[j] <span class="keyword">if</span> j &lt; <span class="built_in">len</span>(row_values) <span class="keyword">else</span> <span class="literal">None</span></span><br><span class="line">        rows.append(row)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> rows:</span><br><span class="line">        errors = bq_client.insert_rows_json(table_ref, rows)</span><br><span class="line">        <span class="keyword">if</span> errors:</span><br><span class="line">            <span class="keyword">raise</span> RuntimeError(<span class="string">f&quot;BigQuery Error: <span class="subst">&#123;errors&#125;</span>&quot;</span>)</span><br><span class="line">    <span class="keyword">return</span> table_ref, <span class="built_in">len</span>(rows)</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="meta">@functions_framework.cloud_event</span></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">handle_drive_event</span>(<span class="params">cloud_event: CloudEvent</span>) -&gt; <span class="literal">None</span>:</span><br><span class="line">    <span class="built_in">print</span>(<span class="string">&quot;=== Cloud Function 実行開始 ===&quot;</span>)</span><br><span class="line">    <span class="built_in">print</span>(</span><br><span class="line">        <span class="string">f&quot;event_id=<span class="subst">&#123;cloud_event.get(<span class="string">&#x27;id&#x27;</span>, <span class="string">&#x27;&#x27;</span>)&#125;</span> type=<span class="subst">&#123;cloud_event.get(<span class="string">&#x27;type&#x27;</span>, <span class="string">&#x27;&#x27;</span>)&#125;</span> subject=<span class="subst">&#123;cloud_event.get(<span class="string">&#x27;subject&#x27;</span>, <span class="string">&#x27;&#x27;</span>)&#125;</span>&quot;</span></span><br><span class="line">    )</span><br><span class="line"></span><br><span class="line">    event_data = &#123;&#125;</span><br><span class="line">    message_data = <span class="string">&quot;&quot;</span></span><br><span class="line">    <span class="keyword">try</span>:</span><br><span class="line">        encoded_data = cloud_event.data.get(<span class="string">&quot;message&quot;</span>, &#123;&#125;).get(<span class="string">&quot;data&quot;</span>, <span class="string">&quot;&quot;</span>)</span><br><span class="line">        <span class="keyword">if</span> encoded_data:</span><br><span class="line">            message_data = base64.b64decode(encoded_data).decode(<span class="string">&quot;utf-8&quot;</span>)</span><br><span class="line">            event_data = json.loads(message_data)</span><br><span class="line">            <span class="built_in">print</span>(<span class="string">f&quot;message_data decoded: <span class="subst">&#123;message_data[:<span class="number">200</span>]&#125;</span>&quot;</span>)</span><br><span class="line">        <span class="keyword">else</span>:</span><br><span class="line">            <span class="built_in">print</span>(<span class="string">&quot;message.data が空です。&quot;</span>)</span><br><span class="line"></span><br><span class="line">        file_id = extract_file_id(cloud_event, event_data)</span><br><span class="line">        <span class="keyword">if</span> <span class="keyword">not</span> file_id:</span><br><span class="line">            attrs = cloud_event.data.get(<span class="string">&quot;message&quot;</span>, &#123;&#125;).get(<span class="string">&quot;attributes&quot;</span>, &#123;&#125;)</span><br><span class="line">            <span class="built_in">print</span>(<span class="string">f&quot;message.attributes=<span class="subst">&#123;attrs&#125;</span>&quot;</span>)</span><br><span class="line">            <span class="built_in">print</span>(<span class="string">f&quot;event_data_keys=<span class="subst">&#123;<span class="built_in">list</span>(event_data.keys())&#125;</span>&quot;</span>)</span><br><span class="line">            <span class="built_in">print</span>(<span class="string">&quot;file_id を抽出できませんでした。処理を終了します。&quot;</span>)</span><br><span class="line">            <span class="keyword">return</span></span><br><span class="line">        <span class="built_in">print</span>(<span class="string">f&quot;file_id=<span class="subst">&#123;file_id&#125;</span>&quot;</span>)</span><br><span class="line">    <span class="keyword">except</span> Exception <span class="keyword">as</span> e:</span><br><span class="line">        <span class="built_in">print</span>(<span class="string">f&quot;Event Parse Error: <span class="subst">&#123;e&#125;</span>&quot;</span>)</span><br><span class="line">        <span class="built_in">print</span>(traceback.format_exc())</span><br><span class="line">        <span class="keyword">return</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">try</span>:</span><br><span class="line">        credentials = Credentials.from_authorized_user_file(<span class="string">&quot;token.json&quot;</span>, SCOPES)</span><br><span class="line">        drive_service = build(<span class="string">&quot;drive&quot;</span>, <span class="string">&quot;v3&quot;</span>, credentials=credentials)</span><br><span class="line">        sheets_service = build(<span class="string">&quot;sheets&quot;</span>, <span class="string">&quot;v4&quot;</span>, credentials=credentials)</span><br><span class="line">        bq_client = bigquery.Client(project=PROJECT_ID)</span><br><span class="line">    <span class="keyword">except</span> Exception <span class="keyword">as</span> e:</span><br><span class="line">        <span class="built_in">print</span>(<span class="string">f&quot;Auth Error: <span class="subst">&#123;e&#125;</span>&quot;</span>)</span><br><span class="line">        <span class="built_in">print</span>(traceback.format_exc())</span><br><span class="line">        <span class="keyword">return</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">try</span>:</span><br><span class="line">        file_meta = drive_service.files().get(fileId=file_id, fields=<span class="string">&quot;id,name,mimeType&quot;</span>).execute()</span><br><span class="line">        mime_type = file_meta.get(<span class="string">&quot;mimeType&quot;</span>)</span><br><span class="line">        <span class="built_in">print</span>(<span class="string">f&quot;file_meta id=<span class="subst">&#123;file_meta.get(<span class="string">&#x27;id&#x27;</span>)&#125;</span> name=<span class="subst">&#123;file_meta.get(<span class="string">&#x27;name&#x27;</span>)&#125;</span> mimeType=<span class="subst">&#123;mime_type&#125;</span>&quot;</span>)</span><br><span class="line">    <span class="keyword">except</span> Exception <span class="keyword">as</span> e:</span><br><span class="line">        <span class="built_in">print</span>(<span class="string">f&quot;Drive API Error: <span class="subst">&#123;e&#125;</span>&quot;</span>)</span><br><span class="line">        <span class="built_in">print</span>(traceback.format_exc())</span><br><span class="line">        <span class="keyword">return</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> mime_type != SPREADSHEET_MIME_TYPE:</span><br><span class="line">        <span class="built_in">print</span>(<span class="string">f&quot;対象外 MIME Type のため終了: <span class="subst">&#123;mime_type&#125;</span>&quot;</span>)</span><br><span class="line">        <span class="keyword">return</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> <span class="keyword">not</span> is_english_filename(file_meta[<span class="string">&quot;name&quot;</span>]):</span><br><span class="line">        <span class="built_in">print</span>(<span class="string">f&quot;英数字ベースのファイル名ではないため終了: <span class="subst">&#123;file_meta[<span class="string">&#x27;name&#x27;</span>]&#125;</span>&quot;</span>)</span><br><span class="line">        <span class="keyword">return</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">try</span>:</span><br><span class="line">        sheets_data = read_spreadsheet(sheets_service, file_id)</span><br><span class="line">        table_ref, row_count = write_spreadsheet_data_by_file_table(bq_client, file_id, file_meta[<span class="string">&quot;name&quot;</span>], sheets_data)</span><br><span class="line">        <span class="built_in">print</span>(<span class="string">f&quot;✅ 同期完了 table=<span class="subst">&#123;table_ref&#125;</span> rows=<span class="subst">&#123;row_count&#125;</span>&quot;</span>)</span><br><span class="line">    <span class="keyword">except</span> Exception <span class="keyword">as</span> e:</span><br><span class="line">        <span class="built_in">print</span>(<span class="string">f&quot;Write Error: <span class="subst">&#123;e&#125;</span>&quot;</span>)</span><br><span class="line">        <span class="built_in">print</span>(traceback.format_exc())</span><br></pre></td></tr></table></figure>

</details>

<h3 id="2-3-Cloud-Function-のデプロイ">2-3. Cloud Function のデプロイ</h3><p>Cloud Functionをデプロイするコマンドです。作成したPub&#x2F;Subトピックがトリガーになるように設定しています。こちらもCloud Shell上で実行してください。</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">gcloud <span class="built_in">functions</span> deploy gws-drive-to-bigquery \</span><br><span class="line">  --gen2 \</span><br><span class="line">  --runtime=python312 \</span><br><span class="line">  --region=asia-northeast1 \</span><br><span class="line">  --<span class="built_in">source</span>=cloud_function/ \</span><br><span class="line">  --entry-point=handle_drive_event \</span><br><span class="line">  --trigger-topic=drive-events-topic \</span><br><span class="line">  --memory=256Mi \</span><br><span class="line">  --<span class="built_in">timeout</span>=120s \</span><br><span class="line">  --max-instances=10 \</span><br><span class="line">  --set-env-vars=<span class="string">&quot;PROJECT_ID=<span class="variable">$&#123;GCP_PROJECT_ID&#125;</span>,DATASET_ID=gws_drive_events&quot;</span> \</span><br><span class="line">  --project=<span class="string">&quot;<span class="variable">$&#123;GCP_PROJECT_ID&#125;</span>&quot;</span></span><br></pre></td></tr></table></figure>

<h2 id="手順3-Workspace-Events-API-サブスクリプション作成">手順3: Workspace Events API サブスクリプション作成</h2><p>ここが今回の肝です。<strong>Workspace Events API</strong> を使って、Drive フォルダの変更イベントを Pub&#x2F;Sub に通知するサブスクリプションを作成します。</p>
<div class="note-container note-warn"><span class="note-icon"></span><div>

<p>注意<br>今回はソースコードを掲載していますが、認証方法などは一例で、環境によっては利用できない場合もあります。<br>自分の環境によって適切なものを選択してください。</p>
</div></div>

<h3 id="3-1-OAuth-2-0-クライアント-ID-の準備">3-1. OAuth 2.0 クライアント ID の準備</h3><ol>
<li>Google Cloud Console を開く</li>
<li>「認証情報を作成」→「OAuth クライアント ID」</li>
<li>アプリケーションの種類: 「デスクトップ アプリ」</li>
<li>JSON をダウンロードして <code>credentials.json</code> として保存</li>
</ol>
<h3 id="3-2-サブスクリプション作成スクリプト">3-2. サブスクリプション作成スクリプト</h3><p>このスクリプトはローカル環境で実行してください。</p>
<details><summary>requirements.txt</summary>

<figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> json</span><br><span class="line"><span class="keyword">import</span> os</span><br><span class="line"><span class="keyword">import</span> sys</span><br><span class="line"><span class="keyword">from</span> urllib.parse <span class="keyword">import</span> urlparse</span><br><span class="line"></span><br><span class="line"><span class="keyword">from</span> google.auth.transport.requests <span class="keyword">import</span> AuthorizedSession, Request</span><br><span class="line"><span class="keyword">from</span> google.oauth2.credentials <span class="keyword">import</span> Credentials</span><br><span class="line"><span class="keyword">from</span> google_auth_oauthlib.flow <span class="keyword">import</span> InstalledAppFlow</span><br><span class="line"><span class="keyword">from</span> requests <span class="keyword">import</span> RequestException</span><br><span class="line"></span><br><span class="line">SCOPES = [</span><br><span class="line">    <span class="string">&quot;https://www.googleapis.com/auth/drive.readonly&quot;</span>,</span><br><span class="line">    <span class="string">&quot;https://www.googleapis.com/auth/spreadsheets.readonly&quot;</span>,  <span class="comment"># CFでのスプレッドシート読み取り用に追加</span></span><br><span class="line">]</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">authenticate</span>(<span class="params">credentials_file=<span class="string">&quot;credentials.json&quot;</span></span>):</span><br><span class="line">    <span class="string">&quot;&quot;&quot;OAuth 2.0 認証&quot;&quot;&quot;</span></span><br><span class="line">    creds = <span class="literal">None</span></span><br><span class="line">    <span class="keyword">if</span> os.path.exists(<span class="string">&quot;token.json&quot;</span>):</span><br><span class="line">        creds = Credentials.from_authorized_user_file(<span class="string">&quot;token.json&quot;</span>, SCOPES)</span><br><span class="line">    <span class="keyword">if</span> <span class="keyword">not</span> creds <span class="keyword">or</span> <span class="keyword">not</span> creds.valid:</span><br><span class="line">        <span class="keyword">if</span> creds <span class="keyword">and</span> creds.expired <span class="keyword">and</span> creds.refresh_token:</span><br><span class="line">            creds.refresh(Request())</span><br><span class="line">        <span class="keyword">else</span>:</span><br><span class="line">            flow = InstalledAppFlow.from_client_secrets_file(credentials_file, SCOPES)</span><br><span class="line">            creds = flow.run_local_server(port=<span class="number">0</span>)</span><br><span class="line">        <span class="keyword">with</span> <span class="built_in">open</span>(<span class="string">&quot;token.json&quot;</span>, <span class="string">&quot;w&quot;</span>) <span class="keyword">as</span> f:</span><br><span class="line">            f.write(creds.to_json())</span><br><span class="line">    <span class="keyword">return</span> creds</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">get_required_env</span>(<span class="params">name</span>):</span><br><span class="line">    <span class="string">&quot;&quot;&quot;必須環境変数を取得（未設定なら分かりやすい例外を返す）&quot;&quot;&quot;</span></span><br><span class="line">    value = os.getenv(name)</span><br><span class="line">    <span class="keyword">if</span> <span class="keyword">not</span> value:</span><br><span class="line">        <span class="keyword">raise</span> RuntimeError(<span class="string">f&quot;環境変数 <span class="subst">&#123;name&#125;</span> が未設定です。&quot;</span> <span class="string">&quot; 実行前に設定してください。&quot;</span>)</span><br><span class="line">    <span class="keyword">return</span> value</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">_get_proxy_summary</span>():</span><br><span class="line">    <span class="string">&quot;&quot;&quot;環境変数のプロキシ設定を安全に要約（認証情報は表示しない）&quot;&quot;&quot;</span></span><br><span class="line">    proxy_url = (</span><br><span class="line">        os.getenv(<span class="string">&quot;HTTPS_PROXY&quot;</span>) <span class="keyword">or</span> os.getenv(<span class="string">&quot;https_proxy&quot;</span>) <span class="keyword">or</span> os.getenv(<span class="string">&quot;HTTP_PROXY&quot;</span>) <span class="keyword">or</span> os.getenv(<span class="string">&quot;http_proxy&quot;</span>)</span><br><span class="line">    )</span><br><span class="line">    <span class="keyword">if</span> <span class="keyword">not</span> proxy_url:</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">None</span></span><br><span class="line"></span><br><span class="line">    parsed = urlparse(proxy_url)</span><br><span class="line">    <span class="keyword">if</span> parsed.hostname <span class="keyword">and</span> parsed.port:</span><br><span class="line">        <span class="keyword">return</span> <span class="string">f&quot;<span class="subst">&#123;parsed.scheme&#125;</span>://<span class="subst">&#123;parsed.hostname&#125;</span>:<span class="subst">&#123;parsed.port&#125;</span>&quot;</span></span><br><span class="line">    <span class="keyword">if</span> parsed.hostname:</span><br><span class="line">        <span class="keyword">return</span> <span class="string">f&quot;<span class="subst">&#123;parsed.scheme&#125;</span>://<span class="subst">&#123;parsed.hostname&#125;</span>&quot;</span></span><br><span class="line">    <span class="keyword">return</span> <span class="string">&quot;(形式不正の可能性あり)&quot;</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">create_subscription</span>(<span class="params">project_id, folder_id</span>):</span><br><span class="line">    <span class="string">&quot;&quot;&quot;Workspace Events API でサブスクリプションを作成&quot;&quot;&quot;</span></span><br><span class="line">    creds = authenticate()</span><br><span class="line">    session = AuthorizedSession(creds)</span><br><span class="line"></span><br><span class="line">    body = &#123;</span><br><span class="line">        <span class="string">&quot;targetResource&quot;</span>: <span class="string">f&quot;//drive.googleapis.com/files/<span class="subst">&#123;folder_id&#125;</span>&quot;</span>,</span><br><span class="line">        <span class="string">&quot;eventTypes&quot;</span>: [</span><br><span class="line">            <span class="string">&quot;google.workspace.drive.file.v3.created&quot;</span>,</span><br><span class="line">            <span class="string">&quot;google.workspace.drive.file.v3.contentChanged&quot;</span>,</span><br><span class="line">            <span class="string">&quot;google.workspace.drive.file.v3.trashed&quot;</span>,</span><br><span class="line">        ],</span><br><span class="line">        <span class="string">&quot;notificationEndpoint&quot;</span>: &#123;</span><br><span class="line">            <span class="string">&quot;pubsubTopic&quot;</span>: <span class="string">f&quot;projects/<span class="subst">&#123;project_id&#125;</span>/topics/drive-events-topic&quot;</span>,</span><br><span class="line">        &#125;,</span><br><span class="line">        <span class="string">&quot;driveOptions&quot;</span>: &#123;</span><br><span class="line">            <span class="string">&quot;includeDescendants&quot;</span>: <span class="literal">True</span>,</span><br><span class="line">        &#125;,</span><br><span class="line">        <span class="string">&quot;payloadOptions&quot;</span>: &#123;</span><br><span class="line">            <span class="string">&quot;includeResource&quot;</span>: <span class="literal">False</span>,</span><br><span class="line">        &#125;,</span><br><span class="line">        <span class="string">&quot;ttl&quot;</span>: <span class="string">&quot;604800s&quot;</span>,</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">try</span>:</span><br><span class="line">        response = session.post(</span><br><span class="line">            <span class="string">&quot;https://workspaceevents.googleapis.com/v1/subscriptions&quot;</span>,</span><br><span class="line">            json=body,</span><br><span class="line">            timeout=<span class="number">30</span>,</span><br><span class="line">        )</span><br><span class="line">        <span class="keyword">if</span> response.status_code &gt;= <span class="number">400</span>:</span><br><span class="line">            <span class="keyword">raise</span> RuntimeError(</span><br><span class="line">                <span class="string">f&quot;Workspace Events API の呼び出しに失敗しました (HTTP <span class="subst">&#123;response.status_code&#125;</span>)。&quot;</span></span><br><span class="line">                <span class="string">f&quot; 詳細: <span class="subst">&#123;response.text&#125;</span>&quot;</span></span><br><span class="line">            )</span><br><span class="line"></span><br><span class="line">        result = response.json()</span><br><span class="line">    <span class="keyword">except</span> OSError <span class="keyword">as</span> e:</span><br><span class="line">        proxy_summary = _get_proxy_summary()</span><br><span class="line">        has_proxy = <span class="built_in">bool</span>(proxy_summary)</span><br><span class="line">        proxy_hint = (</span><br><span class="line">            <span class="string">f&quot;プロキシ環境変数は検出されています (<span class="subst">&#123;proxy_summary&#125;</span>)。&quot;</span></span><br><span class="line">            <span class="keyword">if</span> has_proxy</span><br><span class="line">            <span class="keyword">else</span> <span class="string">&quot;HTTP_PROXY / HTTPS_PROXY が未設定の可能性があります。&quot;</span></span><br><span class="line">        )</span><br><span class="line">        <span class="keyword">raise</span> RuntimeError(</span><br><span class="line">            <span class="string">&quot;ネットワーク接続に失敗しました。&quot;</span></span><br><span class="line">            <span class="string">&quot; 社内ネットワークではプロキシ経由が必要な場合があります。&quot;</span></span><br><span class="line">            <span class="string">f&quot; <span class="subst">&#123;proxy_hint&#125;</span>&quot;</span></span><br><span class="line">            <span class="string">f&quot; 元エラー: <span class="subst">&#123;e&#125;</span>&quot;</span></span><br><span class="line">        ) <span class="keyword">from</span> e</span><br><span class="line">    <span class="keyword">except</span> RequestException <span class="keyword">as</span> e:</span><br><span class="line">        proxy_summary = _get_proxy_summary()</span><br><span class="line">        has_proxy = <span class="built_in">bool</span>(proxy_summary)</span><br><span class="line">        proxy_hint = (</span><br><span class="line">            <span class="string">f&quot;プロキシ環境変数は検出されています (<span class="subst">&#123;proxy_summary&#125;</span>)。&quot;</span></span><br><span class="line">            <span class="keyword">if</span> has_proxy</span><br><span class="line">            <span class="keyword">else</span> <span class="string">&quot;HTTP_PROXY / HTTPS_PROXY が未設定の可能性があります。&quot;</span></span><br><span class="line">        )</span><br><span class="line">        <span class="keyword">raise</span> RuntimeError(</span><br><span class="line">            <span class="string">&quot;HTTPS リクエストに失敗しました。&quot;</span></span><br><span class="line">            <span class="string">&quot; 社内ネットワークではプロキシ経由が必要な場合があります。&quot;</span></span><br><span class="line">            <span class="string">f&quot; <span class="subst">&#123;proxy_hint&#125;</span>&quot;</span></span><br><span class="line">            <span class="string">f&quot; 元エラー: <span class="subst">&#123;e&#125;</span>&quot;</span></span><br><span class="line">        ) <span class="keyword">from</span> e</span><br><span class="line">    <span class="built_in">print</span>(json.dumps(result, indent=<span class="number">2</span>))</span><br><span class="line">    <span class="keyword">return</span> result</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> __name__ == <span class="string">&quot;__main__&quot;</span>:</span><br><span class="line">    <span class="keyword">try</span>:</span><br><span class="line">        project_id = get_required_env(<span class="string">&quot;GCP_PROJECT_ID&quot;</span>)</span><br><span class="line">        folder_id = get_required_env(<span class="string">&quot;DRIVE_FOLDER_ID&quot;</span>)</span><br><span class="line">        create_subscription(project_id, folder_id)</span><br><span class="line">    <span class="keyword">except</span> RuntimeError <span class="keyword">as</span> e:</span><br><span class="line">        <span class="built_in">print</span>(<span class="string">f&quot;ERROR: <span class="subst">&#123;e&#125;</span>&quot;</span>, file=sys.stderr)</span><br><span class="line">        sys.exit(<span class="number">1</span>)</span><br><span class="line"></span><br></pre></td></tr></table></figure>

</details>

<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="comment"># 実行</span></span><br><span class="line">pip install google-api-python-client google-auth-oauthlib</span><br><span class="line">python create_workspace_subscription.py</span><br></pre></td></tr></table></figure>

<div class="note-container note-warn"><span class="note-icon"></span><div>

<p>サブスクリプションの有効期限<br>Workspace Events API のサブスクリプションは <strong>最大7日間</strong> で期限切れになります。本番運用では Cloud Scheduler で 定期的に更新スクリプトを実行してください。</p>
</div></div>

<h2 id="手順4-動作確認">手順4: 動作確認</h2><h3 id="4-1-テスト用スプレッドシートの作成">4-1. テスト用スプレッドシートの作成</h3><p>監視対象フォルダに、以下のようなテスト用スプレッドシートを作成します:</p>
<div class="scroll"><table>
<thead>
<tr>
<th>名前</th>
<th>年齢</th>
<th>部署</th>
</tr>
</thead>
<tbody><tr>
<td>田中太郎</td>
<td>30</td>
<td>開発</td>
</tr>
<tr>
<td>鈴木花子</td>
<td>25</td>
<td>営業</td>
</tr>
</tbody></table></div>
<h3 id="4-2-BigQuery-でデータ確認">4-2. BigQuery でデータ確認</h3><p>スプレッドシートを保存してから数秒〜数十秒後に、BigQuery でデータを確認できます。</p>
<figure class="highlight sql"><table><tr><td class="code"><pre><span class="line"><span class="comment">-- データセットのテーブルを確認</span></span><br><span class="line"><span class="keyword">SELECT</span> table_name</span><br><span class="line"><span class="keyword">FROM</span> `<span class="operator">&lt;</span>project_id<span class="operator">&gt;</span>.gws_drive_events.INFORMATION_SCHEMA.TABLES`;</span><br></pre></td></tr></table></figure>

<div class="note-container note-warn"><span class="note-icon"></span><div>

<p>データが入らない場合<br>まずはCloud Functionのログを確認して処理が実行されているかを確認してください。もし処理が実行されていない場合はPub&#x2F;Subのサブスクリプションでメッセージが配信されているかを確認してください。</p>
</div></div>

<h2 id="注意事項・制限事項">注意事項・制限事項</h2><h3 id="Workspace-Events-API（Developer-Preview）の制限">Workspace Events API（Developer Preview）の制限</h3><div class="scroll"><table>
<thead>
<tr>
<th>項目</th>
<th>制限</th>
</tr>
</thead>
<tbody><tr>
<td>サブスクリプション有効期限</td>
<td>最大 7 日間（要定期更新）</td>
</tr>
<tr>
<td>1ユーザーあたりのサブスクリプション数</td>
<td>上限あり（要確認）</td>
</tr>
<tr>
<td>イベント配信保証</td>
<td>at-least-once（重複ありだが今回は実装で対処）</td>
</tr>
<tr>
<td>プレビュー状態</td>
<td>本番利用は自己責任</td>
</tr>
</tbody></table></div>
<h3 id="Cloud-Function-のサービスアカウント権限">Cloud Function のサービスアカウント権限</h3><p>Cloud Function のサービスアカウントに以下の権限が必要です:</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="comment"># BigQuery Data Editor</span></span><br><span class="line">gcloud projects add-iam-policy-binding <span class="variable">$&#123;GCP_PROJECT_ID&#125;</span> \</span><br><span class="line">  --member=<span class="string">&quot;serviceAccount:<span class="variable">$&#123;GCP_PROJECT_ID&#125;</span>@appspot.gserviceaccount.com&quot;</span> \</span><br><span class="line">  --role=<span class="string">&quot;roles/bigquery.dataEditor&quot;</span></span><br></pre></td></tr></table></figure>

<h2 id="コスト見積もり">コスト見積もり</h2><p>月間 1,000 回のスプレッドシート更新を想定した場合:</p>
<div class="scroll"><table>
<thead>
<tr>
<th>サービス</th>
<th>概算月額コスト</th>
</tr>
</thead>
<tbody><tr>
<td>Cloud Functions</td>
<td>$0〜$1（無料枠内）</td>
</tr>
<tr>
<td>Pub&#x2F;Sub</td>
<td>$0〜$0.1（無料枠内）</td>
</tr>
<tr>
<td>BigQuery（ストレージ）</td>
<td>$0〜$0.5</td>
</tr>
<tr>
<td>BigQuery（クエリ）</td>
<td>使用量による</td>
</tr>
<tr>
<td><strong>合計</strong></td>
<td><strong>ほぼ無料〜$2 程度</strong></td>
</tr>
</tbody></table></div>
<h2 id="まとめ">まとめ</h2><p>Google Workspace Events API（プレビュー）を使うことで、従来の Webhook ベースの方式と比較して、大幅にシンプルなアーキテクチャでリアルタイムデータパイプラインを構築できます。</p>
<h3 id="ポイント">ポイント</h3><ul>
<li>Workspace Events API が Drive → Pub&#x2F;Sub の橋渡しをしてくれる</li>
<li>Cloud Function でサーバーレスに処理</li>
<li>サブスクリプションの 7日間有効期限に注意（要定期更新）</li>
</ul>
<h2 id="参考">参考</h2><ul>
<li>Google Workspace Events API ドキュメント</li>
<li>Workspace Events API - Drive イベント</li>
<li>Cloud Pub&#x2F;Sub ドキュメント</li>
<li>Cloud Functions ドキュメント</li>
<li>Google ドライブ Events API のデベロッパー向け公開プレビュー版が利用可能に</li>
</ul>
]]></content>
    <summary type="html">Google Drive の共有フォルダにあるスプレッドシートが更新されたら、自動的にテーブルとしてBigQueryに取り込みたい。このような仕組みを作りたいと思うことは誰にでもあると思います。従来は Google Drive API の changes.watch を使った Webhook ベースの方法が一般的でしたが...</summary>
    <category term="DataEngineering" scheme="https://future-architect.github.io/categories/DataEngineering/"/>
    <category term="BigQuery" scheme="https://future-architect.github.io/tags/BigQuery/"/>
    <category term="GoogleCloud" scheme="https://future-architect.github.io/tags/GoogleCloud/"/>
    <category term="GoogleWorkspace" scheme="https://future-architect.github.io/tags/GoogleWorkspace/"/>
    <category term="PubSub" scheme="https://future-architect.github.io/tags/PubSub/"/>
    <category term="インデックス" scheme="https://future-architect.github.io/tags/%E3%82%A4%E3%83%B3%E3%83%87%E3%83%83%E3%82%AF%E3%82%B9/"/>
  </entry>
  <entry>
    <title>S3 Tables×AWS Glueで作る次世代データ分析基盤</title>
    <link href="https://future-architect.github.io/articles/20260605a/"/>
    <id>https://future-architect.github.io/articles/20260605a/</id>
    <published>2026-06-04T15:00:00.000Z</published>
    <updated>2026-06-04T15:00:00.000Z</updated>
    <author><name>鈴木風真</name></author>
    <content type="html"><![CDATA[<h2 id="はじめに">はじめに</h2><p>こんにちは！2025年入社で流通サービス事業部の鈴木風真です。</p>
<p>データ分析基盤（データレイク）を構築する中で、現代の企業のデータ分析基盤はどうあるべきか、 <strong>Amazon S3 Tables</strong> を通して、モダンなデータ分析基盤のベストプラクティスを活用事例とともに紹介いたします。</p>
<h2 id="データ分析基盤のベストプラクティスとは">データ分析基盤のベストプラクティスとは</h2><p>多くの企業は、日々の業務を遂行するための基幹システムとは別に、蓄積された売上や発注データを分析し、経営判断に活かすためのデータ分析基盤である「情報系システム（情報基盤）」を構築しています。近年では、そんな情報系システムのあるべき姿として、以下の点が語られます。</p>
<h3 id="基幹システムと情報系システムの「疎結合」">基幹システムと情報系システムの「疎結合」</h3><p>本来、情報系システムは「新たな分析軸を追加したい」といったビジネス側の要求に対し、素早く対応できる必要があります。しかし、絶対に止めることが許されない基幹システムと密結合してしまっていると、改修のたびに厳密な影響調査が必要となり、身動きが取れなくなってしまいます。そのため、両者の間に中立なデータ保管層（データレイク）を挟み、切り離された<strong>疎結合なアーキテクチャ</strong>であることが理想とされています。</p>
<img fetchpriority="high" src="/images/2026/20260605a/Gemini_Generated_Image_ienb2yienb2yienb.png" alt="Gemini_Generated_Image_ienb2yienb2yienb.png" width="1200" height="655">

<p>この「疎結合化」は、単にシステム間の影響を切り離すだけでなく、特定のDWH（データウェアハウス）製品に縛られない柔軟な情報基盤の構築にも直結します。</p>
<p>なぜなら、密結合な状態（基幹システムから特定のDWHの内部ストレージへ直接データを流し込む構成）から脱却し、独立したデータレイク（S3など）にオープンなフォーマットでデータを保持する形になるからです。データという資産の保管場所と、それを分析するDWH（列指向型データベース）が完全に分離されるため、強固なベンダーロックインを回避できます。</p>
<p>これにより、将来的に要件が変わった際にDWHを別の製品にスムーズに乗り換えたり、あるいは用途に応じて複数のDWHを併用するマルチDWH構成を採用したりすることも、疎結合なアーキテクチャであれば容易に実現可能になります。</p>
<h3 id="膨大なデータの長期保持が安価に実現できる">膨大なデータの長期保持が安価に実現できる</h3><p>情報系システムでは、トレンド分析のために10年以上前の過去データを掘り起こすことも珍しくありません。しかし、継続して増え続ける膨大なデータを、一般的なRDBMSや、分析に特化したDWHにすべて保持し続けると、ストレージ費用が莫大なものになってしまいます。「データは長期保管していつでも活用したいが、インフラコストは抑えたい」という、ジレンマを解決することが求められます。</p>
<p>これらの「疎結合化」と「コストを抑えた長期保管」というあるべきを同時に解決するアーキテクチャとして我々が採用したのは、安価なS3をベースにしたデータレイクであり、その後続のDWH連携の要となるのが S3 Tables です。</p>
<h2 id="S3-Tablesとは">S3 Tablesとは</h2><p>Amazon S3 Tablesは、2024年末の「AWS re:Invent」で発表されたばかりの比較的新しい技術で、S3上にフルマネージドなApache Icebergテーブルを構築できるサービスです。</p>
<p>ベースがS3であるためストレージ費用が<strong>非常に安価</strong>であり、膨大なデータの長期保管に最適です。従来のデータレイクで必須だった煩雑なファイルのメンテナンス作業をAWSが自動で行ってくれるため、後続のDWHから直接かつ超高速なデータ分析が可能になります。運用負荷の削減と圧倒的なパフォーマンスを両立できることから、間違いなく今後の<strong>データレイク構築における新定番</strong>になっていくのではないかなと思います。</p>
<h2 id="S3-Tablesでデータレイクを構築する利点">S3 Tablesでデータレイクを構築する利点</h2><h3 id="DWHへのデータのコピーが不要">DWHへのデータのコピーが不要</h3><p>従来のアーキテクチャでは、S3の生ファイル（CSVやJSON）をDWH（例えばAmazon RedshiftやSnowflake）で高速に分析するためには、DWHの内部ストレージにデータをロード（コピー）する必要がありました。</p>
<p>しかし、S3 Tables（Icebergフォーマット）に変換しておけば、RedshiftやSnowflakeなどのモダンなDWHは、S3上のテーブルを直接、しかも内部ストレージと遜色ない超高速なパフォーマンスでクエリできます。 これにより、DWHのストレージコストを大幅に削減し、データ連携の手間を省くことができます。</p>
<h3 id="データ品質が保証される">データ品質が保証される</h3><p>データレイク（S3の生ファイル）は「とりあえず何でも放り込める」のがメリットですが、ファイルの形式が変わったり、カラムが増減したり（スキーマ変更）すると、後続のDWHのロード処理がエラーに陥りがちです。S3 Tablesを通すことで、<strong>型が厳密に定義されたテーブルとしてデータを扱える</strong>ため、DWH側は常に整理された信頼できるデータだけを安心して読み込むことができます。近年では、ただのデータレイク（S3）とDWHの間にIcebergなどのテーブルフォーマット層を挟むアーキテクチャは、「データレイクハウス（Data Lakehouse）」とも呼ばれているようです。</p>
<h2 id="S3-TablesにロードするETLスクリプト-Glue-を自動生成する仕組みを構築">S3 TablesにロードするETLスクリプト(Glue)を自動生成する仕組みを構築</h2><p>前置きが長くなりましたが、こうした背景を踏まえ、今回は基幹システムから受け取ったデータを S3 Tables へ自動投入するパイプラインを構築しました。</p>
<p>要件上、連携される S3 Tables（テーブルの種類）ごとに個別のETLスクリプトを用意する必要がありましたが、それら数十〜数百に及ぶスクリプトをすべてエンジニアが手書きで実装・保守するのは非常に辛く、現実的ではありません。しかし一方で、各スクリプト自体は個別であるものの、その内部で実装すべき型変換や桁数・フォーマットのバリデーションといった処理ロジックはワンパターンであるという性質がありました。</p>
<img src="/images/2026/20260605a/flow.png" alt="flow.png" width="1200" height="439" loading="lazy">

<p>そこで今回は、連携ファイルのインターフェイス定義情報（Excel）をインプットとして、各テーブルに特化したバリデーションチェックやETL処理を含む Glue スクリプトをジェネレータで自動生成。さらに、生成したスクリプトのデプロイまでをワンコマンドで完結できる仕組みを整えました。</p>
<h2 id="S3-TablesにロードするETLスクリプト-Glue">S3 TablesにロードするETLスクリプト(Glue)</h2><p>今回ETLツールとして採用したのはAWS Glueというサービスです。Glue自体はPython(PySpark)でスクリプトを記述することでETLを実装できる仕組みになっています。<br><img src="/images/2026/20260605a/image.png" alt="image.png" width="1200" height="504" loading="lazy"></p>
<h3 id="バリデーションチェック">バリデーションチェック</h3><p>実は、Glueの裏側で動いている分散処理エンジン「Apache Spark」は、型変換（キャスト）の際に不正なデータを受け取ると、デフォルトではエラーで処理を落とさず、サイレントにNULLへ変換して処理を続行する仕様になっています。</p>
<p>従来型のデータレイクであれば「とりあえずエラーで止めずに全件取り込む」という思想も許容されましたが、S3 Tablesは後続のDWHが直接参照する「データレイクハウス」の要です。ここでSparkのデフォルトの挙動のまま不正データをNULLとして混入させてしまうと、後続の分析基盤のデータ品質が著しく低下してしまいます。</p>
<p>不正データはS3 Tablesの手前で確実にエラーとして弾き、綺麗なデータだけをロードするという、DWHと同等の厳格な品質管理を実現するために、ジェネレータを用いたバリデーションの実装が不可欠でした。<br>以下は、ジェネレータが自動出力したバリデーションコードの一部（文字列長チェック）と、エラー時の早期終了処理の抜粋です。</p>
<figure class="highlight py"><table><tr><td class="code"><pre><span class="line"><span class="comment"># 7. バリデーション（フォーマット・桁数チェック）の一部抜粋</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 発注区分（ORDER_TYPE）: 文字列長バリデーション（最大2文字）</span></span><br><span class="line">_err_df = df.<span class="built_in">filter</span>(F.length(F.col(<span class="string">&quot;ORDER_TYPE&quot;</span>)) &gt; <span class="number">2</span>)</span><br><span class="line">_cnt = _err_df.count()</span><br><span class="line"><span class="keyword">if</span> _cnt &gt; <span class="number">0</span>:</span><br><span class="line">    <span class="comment"># エラーレコードの特定とサンプリング処理（PKとなる情報と共にログ出力用配列へ格納）</span></span><br><span class="line">    _sample = _err_df.select(<span class="string">&quot;ORDER_TYPE&quot;</span>, <span class="string">&quot;ORDER_DATE&quot;</span>, <span class="string">&quot;CUSTOMER_ID&quot;</span>, <span class="string">&quot;ORDER_ID&quot;</span>, <span class="string">&quot;ITEM_CD&quot;</span>).limit(<span class="number">20</span>).collect()</span><br><span class="line">    _lines = []</span><br><span class="line">    <span class="keyword">for</span> i, r <span class="keyword">in</span> <span class="built_in">enumerate</span>(_sample):</span><br><span class="line">        _pk_parts = [k + <span class="string">&quot;=&#x27;&quot;</span> + <span class="built_in">str</span>(v) + <span class="string">&quot;&#x27;&quot;</span> <span class="keyword">for</span> k, v <span class="keyword">in</span> r.asDict().items() <span class="keyword">if</span> k != <span class="string">&quot;ORDER_TYPE&quot;</span>]</span><br><span class="line">        _lines.append(<span class="string">f&quot;  (<span class="subst">&#123;i+<span class="number">1</span>&#125;</span>) ORDER_TYPE=&#x27;<span class="subst">&#123;r[<span class="string">&#x27;ORDER_TYPE&#x27;</span>]&#125;</span>&#x27; | PK: <span class="subst">&#123;<span class="string">&#x27;, &#x27;</span>.join(_pk_parts)&#125;</span>&quot;</span>)</span><br><span class="line">    validation_errors.append(<span class="string">&quot;[ORDER_TYPE] len&gt;2 NG &quot;</span> + <span class="built_in">str</span>(_cnt) + <span class="string">&quot;件\n&quot;</span> + <span class="string">&quot;\n&quot;</span>.join(_lines))</span><br><span class="line"></span><br><span class="line"><span class="comment"># （中略：他の全カラムに対するチェック処理が続く...）</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 8. バリデーション結果チェック（早期終了）</span></span><br><span class="line"><span class="keyword">if</span> validation_errors:</span><br><span class="line">    <span class="built_in">print</span>(<span class="string">&quot;===== Validation Error Summary =====&quot;</span>, flush=<span class="literal">True</span>)</span><br><span class="line">    <span class="keyword">for</span> _err <span class="keyword">in</span> validation_errors:</span><br><span class="line">        <span class="built_in">print</span>(_err, flush=<span class="literal">True</span>)</span><br><span class="line">    <span class="keyword">raise</span> ValueError(<span class="string">f&quot;バリデーションエラー: <span class="subst">&#123;<span class="built_in">len</span>(validation_errors)&#125;</span>件の問題があります。詳細はログを確認してください&quot;</span>)</span><br></pre></td></tr></table></figure>

<p>ただエラーで弾いて落とすだけでなく、運用フェーズで原因調査がしやすいように「どのレコードの、どの主キー（PK群）でエラーが起きたか」を最大20件サンプリングしてログに書き出す工夫を入れています。</p>
<figure><img src="/images/2026/20260605a/image_2.png" alt="image.png" width="1200" height="564" loading="lazy"><figcaption>↑エラー時のGlueの画面</figcaption></figure>
<figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">===== Validation Error Summary =====</span><br><span class="line"></span><br><span class="line">[ORDER_TYPE] len&gt;2 1</span><br><span class="line">  (1) ORDER_TYPE=<span class="string">&#x27;AＢC&#x27;</span> | PK: ORDER_DATE=<span class="string">&#x27;None&#x27;</span>, CUSTOMER_ID=<span class="string">&#x27;CUST1001&#x27;</span>, ORDER_ID=<span class="string">&#x27;ORD1001&#x27;</span>, ITEM_CD=<span class="string">&#x27;ITEM100001&#x27;</span></span><br><span class="line"></span><br><span class="line">=====================================</span><br></pre></td></tr></table></figure>

<p>↑詳細なエラーログ<br>ORDER_TYPEの最大2文字という制約に引っかかったデータ行が1つあること、そしてそのデータのPK項目が示されます。これにより、ユーザーはデータのどこに不正があったかを確認できます。</p>
<p>バリデーションチェックはカラムごとに実装しますが、「これをテーブルの全カラム分（数百カラム）手書きするのは絶対にやりたくない！」と思いました。コピペで量産しようものなら、保守性は最悪です。</p>
<p>本システムでは、インターフェース定義書（Excel）に「型：文字列、最大桁数：2」と記載しておくだけで、ジェネレータが上記のようなチェックロジックをカラムの数だけ動的に組み立ててくれます。これにより、人的ミスを完全に排除しつつ、実運用に耐えうる保守性を担保しています。<br>※ジェネレータのコードは非公開です。</p>
<h3 id="二重取込防止">二重取込防止</h3><p>S3 Tablesに取り込まれるファイルが二重で取り込まれないようにする仕組みも実装しました。例えば、送信元のシステムが間違えて同じファイルを2回アップロードしてしまった場合や、リトライ処理が走った場合、何も対策していないとS3 Tablesに同じデータが重複してインサートされてしまいます。</p>
<p>そこで今回のスクリプトでは、Icebergフォーマットの恩恵を活かし、データの投入（Load）直前に以下のようなチェック処理を実装しています。</p>
<figure class="highlight py"><table><tr><td class="code"><pre><span class="line"><span class="comment"># 12. Amazon S3 Tables へのロード (Load)</span></span><br><span class="line">namespace = <span class="string">&quot;default&quot;</span></span><br><span class="line">table_name = <span class="string">&quot;uriage&quot;</span></span><br><span class="line">full_table_identifier = <span class="string">f&quot;s3tables.<span class="subst">&#123;namespace&#125;</span>.<span class="subst">&#123;table_name&#125;</span>&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 二重取込防止: 同名ファイルが既に存在する場合はエラー終了</span></span><br><span class="line"><span class="keyword">if</span> source_file:</span><br><span class="line">    <span class="comment"># S3 Tablesに対して直接SQLを発行し、同一ファイル名が取り込み済みかチェック</span></span><br><span class="line">    _existing_count = spark.sql(<span class="string">f&quot;SELECT COUNT(*) FROM <span class="subst">&#123;full_table_identifier&#125;</span> WHERE source_file = &#x27;<span class="subst">&#123;source_file&#125;</span>&#x27;&quot;</span>).collect()[<span class="number">0</span>][<span class="number">0</span>]</span><br><span class="line">    <span class="keyword">if</span> _existing_count &gt; <span class="number">0</span>:</span><br><span class="line">        <span class="keyword">raise</span> ValueError(<span class="string">f&quot;二重取込エラー: <span class="subst">&#123;source_file&#125;</span> は既に <span class="subst">&#123;_existing_count&#125;</span>件 存在します&quot;</span>)</span><br><span class="line"></span><br><span class="line"><span class="comment"># データ書き込み（取込モード: 追記）</span></span><br><span class="line">df.writeTo(full_table_identifier) \</span><br><span class="line">    .using(<span class="string">&quot;iceberg&quot;</span>) \</span><br><span class="line">    .append()</span><br><span class="line"></span><br><span class="line">job.commit()</span><br></pre></td></tr></table></figure>

<p>従来のS3上のParquetファイル群に対して同様の重複チェックを行う場合、Athenaを叩いたり、S3のオブジェクト一覧を取得したりと一手間必要でした。しかし、S3 TablesはフルマネージドなIcebergテーブルとして機能するため、PySparkの spark.sql を使って、ロード先のテーブルを直接かつ高速にクエリできます。</p>
<p>ここで取り込み対象のファイル名（source_file）が既にテーブル内に存在するかをカウントし、存在する場合は意図的に<code>ValueError</code>を発生させてジョブを落とします。<br>そして、安全が確認されたデータのみが、<code>.writeTo().using(&quot;iceberg&quot;).append()</code>というAPIによってS3 Tablesへ投入されます。</p>
<div class="note-container note-info"><span class="note-icon"></span><div>

<p>なぜGlue標準の「Job Bookmark」を使わなかったのか？</p>
<p>実はAWS Glueには「Job Bookmark」という機能があり、これを有効にするとすでに処理した入力ファイルを自動的にスキップ（重複排除）してくれます。一見するとこの標準機能を使えば良いように思えますが、今回のアーキテクチャではあえて不採用としました。</p>
<p>その理由は、Bookmark機能が重複ファイルを検知した際の <strong>「ファイル読み込みは成功するが、中身が0件の空データとして扱われる」</strong> という仕様にあります。テーブルのデータを全件入れ替える「洗替型（Overwrite）」のバッチ処理にこの仕様が適用された場合、空のデータで既存テーブルを上書きしてしまう（＝テーブルのデータが全件消失する）という致命的な事故に繋がります。</p>
</div></div>

<h2 id="環境に依存しない一括デプロイ">環境に依存しない一括デプロイ</h2><p>自動生成されたGlueスクリプトは、AWS環境（S3へのスクリプト配置やGlueジョブの登録）へデプロイして初めて機能します。しかし、数十個のジョブをマネジメントコンソールから手作業でポチポチと登録していくのは、面倒です！</p>
<p>そこで今回のシステムでは、出力したGlueスクリプト群をまとめてデプロイするための AWS CLIデプロイコマンド（シェルスクリプト）も、ジェネレータに一緒に生成させるようにしました。</p>
<p>このデプロイコマンドには、以下2つの工夫が組み込まれています。</p>
<h3 id="1-デプロイ対象（機能ID）の絞り込み">1. デプロイ対象（機能ID）の絞り込み</h3><p>ジェネレータが生成したシェルスクリプト内には、対象となる機能ID（ジョブ名）の配列が自動で組み込まれます。これにより、デプロイコマンドを実行するだけで、対象機能のスクリプトだけがS3にアップロードされ、Glueジョブとして作成（または更新）される仕組みになっています。</p>
<h3 id="2-実行時パラメータ（環境変数）による環境差異の吸収">2. 実行時パラメータ（環境変数）による環境差異の吸収</h3><p>開発環境（dev）、テスト環境（test）、本番環境（prod）では、利用するIAMロールやS3バケット名が当然異なります。これらをスクリプト内にハードコーディングせず、コマンド実行時の引数（DEPLOY_ENV&#x3D;dev など）に応じて設定ファイル（.env）から動的に読み込むようにしました。これにより、環境を切り替えるだけで、Glueジョブ内の設定（S3 TablesのバケットARNなど）が自動的に適切な値に置き換わります。</p>
<p>実際のシェルスクリプトのコア部分（抜粋）は以下のようになっています。</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="meta">#!/bin/bash</span></span><br><span class="line"><span class="comment"># 実行環境の指定（例: DEPLOY_ENV=dev なら config/dev.env を読み込む）</span></span><br><span class="line">: <span class="string">&quot;<span class="variable">$&#123;DEPLOY_ENV:?DEPLOY_ENV must be set (dev|test|prod)&#125;</span>&quot;</span></span><br><span class="line">ENV_FILE=<span class="string">&quot;<span class="variable">$&#123;SCRIPT_DIR&#125;</span>/../../../config/<span class="variable">$&#123;DEPLOY_ENV&#125;</span>.env&quot;</span></span><br><span class="line">. <span class="string">&quot;<span class="variable">$ENV_FILE</span>&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># ==========================================</span></span><br><span class="line"><span class="comment"># 1. JOB_NAMESに含まれるスクリプトの個別アップロードとGlueジョブの作成/更新</span></span><br><span class="line"><span class="comment"># ==========================================</span></span><br><span class="line"><span class="built_in">echo</span> <span class="string">&quot;=== Uploading selected scripts and Creating/Updating Glue Jobs ===&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># ジェネレータによって、今回デプロイ対象となる機能IDが自動的に配列として出力される</span></span><br><span class="line">JOB_NAMES=(</span><br><span class="line">    <span class="string">&quot;glue-job-0001&quot;</span></span><br><span class="line">    <span class="string">&quot;glue-job-0007&quot;</span></span><br><span class="line">    <span class="string">&quot;glue-job-00020&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span> JOB_NAME <span class="keyword">in</span> <span class="string">&quot;<span class="variable">$&#123;JOB_NAMES[@]&#125;</span>&quot;</span>; <span class="keyword">do</span></span><br><span class="line">    SCRIPT_LOCATION=<span class="string">&quot;<span class="variable">$&#123;S3_SCRIPT_BUCKET&#125;</span>/<span class="variable">$&#123;JOB_NAME&#125;</span>.py&quot;</span></span><br><span class="line">    <span class="built_in">echo</span> <span class="string">&quot;Processing job: <span class="variable">$&#123;JOB_NAME&#125;</span>&quot;</span></span><br><span class="line"></span><br><span class="line">    <span class="comment"># ① 生成されたスクリプトをS3のスクリプトバケットへアップロード</span></span><br><span class="line">    aws s3 <span class="built_in">cp</span> <span class="string">&quot;../scripts/<span class="variable">$&#123;JOB_NAME&#125;</span>.py&quot;</span> <span class="string">&quot;<span class="variable">$&#123;SCRIPT_LOCATION&#125;</span>&quot;</span></span><br><span class="line"></span><br><span class="line">    <span class="comment"># ② Glueジョブの存在確認</span></span><br><span class="line">    <span class="keyword">if</span> aws glue get-job --job-name <span class="string">&quot;<span class="variable">$&#123;JOB_NAME&#125;</span>&quot;</span> &gt; /dev/null 2&gt;&amp;1; <span class="keyword">then</span></span><br><span class="line">        <span class="built_in">echo</span> <span class="string">&quot;  Updating existing job...&quot;</span></span><br><span class="line">        <span class="comment"># 既存ジョブの更新（Update）</span></span><br><span class="line">        aws glue update-job \</span><br><span class="line">            --job-name <span class="string">&quot;<span class="variable">$&#123;JOB_NAME&#125;</span>&quot;</span> \</span><br><span class="line">            --job-update <span class="string">&quot;&#123;</span></span><br><span class="line"><span class="string">                \&quot;Role\&quot;: \&quot;<span class="variable">$&#123;GLUE_ROLE_ARN&#125;</span>\&quot;,</span></span><br><span class="line"><span class="string">                \&quot;Command\&quot;: &#123;</span></span><br><span class="line"><span class="string">                    \&quot;Name\&quot;: \&quot;glueetl\&quot;,</span></span><br><span class="line"><span class="string">                    \&quot;ScriptLocation\&quot;: \&quot;<span class="variable">$&#123;SCRIPT_LOCATION&#125;</span>\&quot;,</span></span><br><span class="line"><span class="string">                    \&quot;PythonVersion\&quot;: \&quot;3\&quot;</span></span><br><span class="line"><span class="string">                &#125;,</span></span><br><span class="line"><span class="string">                \&quot;DefaultArguments\&quot;: &#123;</span></span><br><span class="line"><span class="string">                    \&quot;--extra-jars\&quot;: \&quot;<span class="variable">$&#123;DEPENDENT_JARS_PATH&#125;</span>\&quot;,</span></span><br><span class="line"><span class="string">                    \&quot;--table_bucket_arn\&quot;: \&quot;<span class="variable">$&#123;TABLE_BUCKET_ARN&#125;</span>\&quot;</span></span><br><span class="line"><span class="string">                &#125;,</span></span><br><span class="line"><span class="string">                \&quot;GlueVersion\&quot;: \&quot;<span class="variable">$&#123;GLUE_VERSION&#125;</span>\&quot;,</span></span><br><span class="line"><span class="string">                \&quot;WorkerType\&quot;: \&quot;<span class="variable">$&#123;WORKER_TYPE&#125;</span>\&quot;,</span></span><br><span class="line"><span class="string">                \&quot;NumberOfWorkers\&quot;: <span class="variable">$&#123;NUMBER_OF_WORKERS&#125;</span></span></span><br><span class="line"><span class="string">            &#125;&quot;</span></span><br><span class="line">    <span class="keyword">else</span></span><br><span class="line">        <span class="built_in">echo</span> <span class="string">&quot;  Creating new job...&quot;</span></span><br><span class="line">        <span class="comment"># 新規ジョブの作成（Create）</span></span><br><span class="line">        aws glue create-job \</span><br><span class="line">            --name <span class="string">&quot;<span class="variable">$&#123;JOB_NAME&#125;</span>&quot;</span> \</span><br><span class="line">            --role <span class="string">&quot;<span class="variable">$&#123;GLUE_ROLE_ARN&#125;</span>&quot;</span> \</span><br><span class="line">            --<span class="built_in">command</span> <span class="string">&quot;&#123;</span></span><br><span class="line"><span class="string">                \&quot;Name\&quot;: \&quot;glueetl\&quot;,</span></span><br><span class="line"><span class="string">                \&quot;ScriptLocation\&quot;: \&quot;<span class="variable">$&#123;SCRIPT_LOCATION&#125;</span>\&quot;,</span></span><br><span class="line"><span class="string">                \&quot;PythonVersion\&quot;: \&quot;3\&quot;</span></span><br><span class="line"><span class="string">            &#125;&quot;</span> \</span><br><span class="line">            --default-arguments <span class="string">&quot;&#123;\\\&quot;--extra-jars\\\&quot;:\\\&quot;<span class="variable">$&#123;DEPENDENT_JARS_PATH&#125;</span>\\\&quot;,\\\&quot;--table_bucket_arn\\\&quot;:\\\&quot;<span class="variable">$&#123;TABLE_BUCKET_ARN&#125;</span>\\\&quot;&#125;&quot;</span> \</span><br><span class="line">            --glue-version <span class="string">&quot;<span class="variable">$&#123;GLUE_VERSION&#125;</span>&quot;</span> \</span><br><span class="line">            --worker-type <span class="string">&quot;<span class="variable">$&#123;WORKER_TYPE&#125;</span>&quot;</span> \</span><br><span class="line">            --number-of-workers <span class="variable">$&#123;NUMBER_OF_WORKERS&#125;</span></span><br><span class="line">    <span class="keyword">fi</span></span><br><span class="line">    <span class="built_in">echo</span> <span class="string">&quot;  Done: <span class="variable">$&#123;JOB_NAME&#125;</span>&quot;</span></span><br><span class="line"><span class="keyword">done</span></span><br></pre></td></tr></table></figure>

<p><strong>【このコードのポイント】</strong><br>aws glue create-job (または update-job) を実行する際、–default-arguments パラメータを通じて、環境変数から取得した ${TABLE_BUCKET_ARN} などの値をGlueジョブに渡し込んでいます。</p>
<p>この仕組みにより、開発者は「Excelでテーブルを定義する」→「ジェネレータを実行する」→「環境を指定してデプロイコマンドを叩く」というわずか3ステップで、S3 Tablesへデータを投入するデータパイプラインを本番環境へ安全に展開できるようになりました。</p>
<p>例えば、デプロイコマンドで DEPLOY_ENV&#x3D;dev と指定した際に読み込まれる、開発環境用の設定ファイル（dev.env）の中身は以下のようになっています。</p>
<figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">Properties</span><br><span class="line"><span class="comment"># Shared settings for development environment (dev.env)</span></span><br><span class="line">S3_SCRIPT_BUCKET=s3://aws-glue-assets-&lt;AWS_ACCOUNT_ID&gt;-ap-northeast-1/scripts</span><br><span class="line">ASSUME_ROLE_ARN=arn:aws:iam::&lt;AWS_ACCOUNT_ID&gt;:role/DataAnalyticsRole</span><br><span class="line">GLUE_ROLE_ARN=arn:aws:iam::&lt;AWS_ACCOUNT_ID&gt;:role/DataAnalyticsServiceRoleForGlue</span><br><span class="line"></span><br><span class="line"><span class="comment"># Glueジョブの基本スペック設定</span></span><br><span class="line">GLUE_VERSION=5.0</span><br><span class="line">WORKER_TYPE=G.1X</span><br><span class="line">NUMBER_OF_WORKERS=2</span><br><span class="line"></span><br><span class="line"><span class="comment"># S3 Tables (Iceberg) 連携用の必須設定</span></span><br><span class="line">DEPENDENT_JARS_PATH=s3://&lt;YOUR_BUCKET_PREFIX&gt;-landing-zone/s3-tables-catalog-for-iceberg-runtime-0.1.8.jar</span><br><span class="line">TABLE_BUCKET_ARN=arn:aws:s3tables:ap-northeast-1:&lt;AWS_ACCOUNT_ID&gt;:bucket/&lt;YOUR_TABLE_BUCKET_NAME&gt;</span><br></pre></td></tr></table></figure>

<p><strong>【この設定ファイルのポイント】</strong></p>
<p>単なる変数の外出しに見えますが、S3 Tablesを扱う上で非常に重要な設定が含まれています。</p>
<p>DEPENDENT_JARS_PATH の指定<br>Glue（PySpark）からS3 Tables上のIcebergテーブルを操作するためには、専用のランタイムライブラリ（s3-tables-catalog-for-iceberg-runtime-*.jar）が必要です。このJARファイルのS3パスを環境変数として切り出しておくことで、将来ライブラリのバージョンアップが必要になった際も、コードを修正することなく .env のファイル名を書き換えるだけで対応できます。</p>
<p><strong>TABLE_BUCKET_ARN の扱い</strong></p>
<p>S3 Tablesは、従来のS3（s3:&#x2F;&#x2F;…）とは異なり、専用の Table Bucket ARN（arn:aws:s3tables:…）を用いてアクセス先を指定します。開発環境・テスト環境・本番環境でこのARNは確実に変わるため、環境変数からGlueジョブの –table_bucket_arn パラメータへ動的に注入するこの設計が非常に活きてきます。</p>
<p>このように、AWSのインフラ情報やライブラリのパスを .env に集約させることで、ジェネレータが生成するGlueスクリプト本体には環境依存のハードコードが一切含まれない、パイプラインを実現しています。</p>
<h2 id="おわりに">おわりに</h2><p>S3 TablesとAWS Glueを利用し、基幹システムと疎結合かつ安価なデータレイクを構築する手法をご紹介しました。<br>データ基盤の最適化や、膨大なETLパイプラインの実装・運用に悩む方々にとって、本記事のアプローチが少しでも参考になれば幸いです。</p>
<p>最後までお読みいただき、ありがとうございました！</p>
]]></content>
    <summary type="html">モダンなデータ分析基盤のベストプラクティスについて、S3 Tabalesの活用事例とともに紹介いたします。</summary>
    <category term="DataEngineering" scheme="https://future-architect.github.io/categories/DataEngineering/"/>
    <category term="AWS" scheme="https://future-architect.github.io/tags/AWS/"/>
    <category term="Glue" scheme="https://future-architect.github.io/tags/Glue/"/>
    <category term="S3" scheme="https://future-architect.github.io/tags/S3/"/>
    <category term="データレイク" scheme="https://future-architect.github.io/tags/%E3%83%87%E3%83%BC%E3%82%BF%E3%83%AC%E3%82%A4%E3%82%AF/"/>
  </entry>
  <entry>
    <title>データマネジメント設計ガイドラインを公開しました</title>
    <link href="https://future-architect.github.io/articles/20251229a/"/>
    <id>https://future-architect.github.io/articles/20251229a/</id>
    <published>2025-12-28T15:00:00.000Z</published>
    <updated>2025-12-28T15:00:00.000Z</updated>
    <author><name>中神孝士</name></author>
    <content type="html"><![CDATA[<img fetchpriority="high" src="/images/2025/20251229a/top.png" alt="" width="700" height="391">

<h2 id="はじめに">はじめに</h2><p>こんにちは、TIG中神です。<br>フューチャー社内の有志のメンバーでデータマネジメント設計ガイドラインを作成しました。</p>

<img src="/images/2025/20251229a/contents.png" alt="" width="500" height="206" loading="lazy">


<p>https://future-architect.github.io/arch-guidelines/documents/forDataManagement/datamanagement_guidelines.html</p>
<p>この記事では、ガイドライン策定の目的と、その内容を抜粋して紹介します。</p>
<h2 id="ガイドライン策定の目的">ガイドライン策定の目的</h2><p>データマネジメントはデータ利活用を促進し、ビジネス価値を最大化させるための組織的な活動です。昨今の生成AIの急速な普及により、AI Readyでセキュアなデータマネジメントの重要性はさらに増しています。</p>
<p>しかし、いざデータ基盤を構築しようとすると、「権限制御をどこまで厳格にすべきか」「組織の成長に合わせて誰が何を管理すべきか」といった、技術・組織・運用の三者が絡み合う設計判断に直面します。</p>
<p>本ガイドラインは、こうした「現場のエンジニアや設計者が悩む実務的なポイント」に対する議論のベースラインを提供することを目的として策定されました。</p>
<h3 id="想定活用シーン">想定活用シーン</h3><ul>
<li>データ基盤の新規立ち上げ時における全体方針の策定</li>
<li>組織拡大に伴うデータガバナンス（権限制御やロール定義）の再設計</li>
<li>ETLやデータカタログなどの技術スタック選定時の比較検討</li>
</ul>
<p>このガイドラインでは抽象的な理論に留まらず、具体的な設計案と評価観点を示すことで、設計者の迷いを減らし、意思決定を加速させることを目指しています。</p>
<h2 id="ガイドラインの内容のご紹介">ガイドラインの内容のご紹介</h2><p>本ガイドラインは、データマネジメントの知識体系（DMBOK2）を参考にしつつ、よりシステム構築現場に近い視点で構成されています。<br>ガイドラインの主要なポイントを要約して紹介します。</p>
<h3 id="データガバナンスモデル">データガバナンスモデル</h3><p>分散型・中央集権型・連合型（データメッシュ）の比較。組織の成熟度に応じたシナリオを提示。</p>
<h3 id="組織・ロール定義">組織・ロール定義</h3><p>データオーナーやデータスチュワードなど、組織規模によって変化する役割分担を定義。</p>
<h3 id="データカタログ・メタデータ">データカタログ・メタデータ</h3><p>データの発見性を高めるカタログの導入プロセスと、管理すべきメタデータのモデルを詳説。</p>
<h3 id="データレイヤー・ETL">データレイヤー・ETL</h3><p>ETL&#x2F;ELTの使い分け、ゼロETL、MDM（マスタデータ管理）など、データパイプラインの設計指針。</p>
<h3 id="データ品質・プロファイリング">データ品質・プロファイリング</h3><p>「だれにとっての品質か」という視点や、データコントラクトによる品質担保の考え方。</p>
<h3 id="認証・権限制御">認証・権限制御</h3><p>最も実務的な難所。列レベルの可視性制御や、組織変更に強いロール設計パターン。<br>シンプルなパターンから、部署限定・役職限定などの複雑な要件をどう技術的に落とし込むか、具体的なモデル案を記載。</p>
<h3 id="運用・監査">運用・監査</h3><p>データリネージの自動設定や、組織変更・棚卸しといったライフサイクル管理。</p>
<h2 id="さいごに">さいごに</h2><p>今回紹介した内容はガイドラインの一部です。社内のメンバーはもちろん、社外の方々にも設計のベースラインとしてご活用いただければ幸いです。</p>
<p>またフィードバックやPRについてもお待ちしております。</p>
<div class="scroll"><table>
<thead>
<tr>
<th align="left">GitHubリポジトリ</th>
<th align="left">ドキュメントページ</th>
</tr>
</thead>
<tbody><tr>
<td align="left"><img src="https://img.shields.io/badge/GitHub-future--architect%2Farch--guidelines-24292f?style=for-the-badge&logo=github" alt="GitHub Repo" width="379" height="28" loading="lazy"></td>
<td align="left"><img src="https://img.shields.io/badge/Docs-Future%20Architecture%20Guidelines-0969da?style=for-the-badge&logo=gitbook&logoColor=white" alt="Guidelines Page" width="346" height="28" loading="lazy"></td>
</tr>
</tbody></table></div>
<p>最後に、本ガイドラインの作成に貢献頂いた有志のみなさんに感謝します。</p>
<p>データマネジメント領域は技術のやトレンド移り変わりが速く、関連用語や実現するため技術・プロダクトは構想検討や設計時点で常に情報をアップデートしていく事が望ましいです。</p>
<p>ただ、データマネジメントを構成する設計要素やポイントは汎用的であり普遍的なものですので構想検討や設計のベースラインとしては利活用できるものにはなっているのではないかと思います。</p>
<p>データマネジメントは一度構築して終わりではなく、ビジネスの成長に合わせて進化し続けるものです。本ガイドラインも、今後の技術トレンドや新たなユースケース（非構造化データの高度な活用など）を反映し、継続的にアップデートを図っていく予定です。<br>ですが「ここ、もう古くなってるよ」とか「最新の考えはこう」という内容を見つけられましたらぜひPR頂けますと大変ありがたいです。</p>
<p>本ドキュメントが、データマネジメントに関わるエンジニアやデータスチュワードの方々にとって、より良いデータ利活用環境を築くための「道標」となれば幸いです。</p>
]]></content>
    <summary type="html">フューチャー社内の有志のメンバーでデータマネジメント設計ガイドラインを作成しました。</summary>
    <category term="DataEngineering" scheme="https://future-architect.github.io/categories/DataEngineering/"/>
    <category term="ガイドライン" scheme="https://future-architect.github.io/tags/%E3%82%AC%E3%82%A4%E3%83%89%E3%83%A9%E3%82%A4%E3%83%B3/"/>
    <category term="データマネジメント" scheme="https://future-architect.github.io/tags/%E3%83%87%E3%83%BC%E3%82%BF%E3%83%9E%E3%83%8D%E3%82%B8%E3%83%A1%E3%83%B3%E3%83%88/"/>
  </entry>
  <entry>
    <title>AI-ReadyのためのAI駆動のデータモデリング</title>
    <link href="https://future-architect.github.io/articles/20251105a/"/>
    <id>https://future-architect.github.io/articles/20251105a/</id>
    <published>2025-11-04T15:00:00.000Z</published>
    <updated>2025-11-04T15:00:00.000Z</updated>
    <author><name>大前七奈</name></author>
    <content type="html"><![CDATA[<img fetchpriority="high" src="/images/2025/20251105a/hierarchical_data_modeling_conce.png" alt="hierarchical_data_modeling_conce.png" width="1024" height="1024">

<p>こんにちは。製造エネルギー事業部の大前七奈です。</p>
<p>今年に入り、GeminiなどLLMが今までにない進化を遂げています。多くの企業がAI活用を模索する一方で、「AIに何を食べさせればよいか分からない」「データが散在・サイロ化していて使えない」といった課題に直面しています。フューチャー恒例の秋のブログ週間を機に、今回はAI（特にGEMINI-CLI）を利用して、どのようにAIと協働してデータモデリングできるか、直近のデータモデリングのトレンドを調べました。</p>
<p>本記事では、<strong>AI活用のROIを最大化するために、なぜ「概念モデル」からデータモデリングを始めるべきか</strong>、そして、<strong>AI自体をモデリングのパートナーとして活用し、コストパフォーマンス高くアジャイルにデータ基盤を整備する具体的な手法</strong>を紹介します。</p>
<p>※GEMINI-CLI自体を利用するときの注意点やテクニックについてはまた別記事にまとめるため、ここで割愛させていただきます。</p>
<h2 id="従来のデータ統合問題">従来のデータ統合問題</h2><p>レガシーシステム統合時、データ品質問題や設計上の問題に加え、最も困難な課題として「情報の意味（セマンティクス）」の不一致が挙げられます。新しいシステムと古いシステム間、あるいは、異なる事業部や部門で、同じエンティティが異なる意味や粒度で定義されている場合、技術的な接続以上にセマンティクスの統合がコストを増大させます。いわば、「表現のゆらぎ」問題です。</p>
<ul>
<li>よくある表現のゆらぎ問題＝領域による同じ言葉の違い:<ul>
<li>営業システムの <code>customers</code> テーブル（見込み客を含む）</li>
<li>経理システムの <code>customers</code> テーブル（取引先のみ）</li>
<li>サポートシステムの <code>customers</code> テーブル（保守契約企業のみ）</li>
</ul>
</li>
</ul>
<h2 id="AI時代のためのデータモデリング：概念モデルから始めるコスト効率">AI時代のためのデータモデリング：概念モデルから始めるコスト効率</h2><p>AIは手段です。その能力を最大限に引き出すには、良質なデータが必要ですが、やみくもに全データを統合するのは得策ではありません。結局誰も使わないデータ基盤が完成し、投資対効果が見合わなくなります。</p>
<p>そこで重要になるのが、<strong>価値に焦点を当てたトップダウンのアプローチ</strong>です。ER図やテーブル定義といった物理的な設計から入る前に、まず企業全体の<strong>バリューチェーン（概念モデル）</strong>を定義することで、AI活用のコストパフォーマンスを最大化できます。</p>
<pre class="mermaid" data-mermaid="98bca3463b2869655ee5b404445e4dd67c5f9ed248cd35648bd4488b453a5463">graph TD
    Conceptual_Model[バリューチェーン<br>＝概念モデル]-->|価値の源泉を特定|Logical_Model[データフローを表すER図<br>＝論理モデル]
    Logical_Model-->|業務機能間の連携を定義|Physical_Model[テーブル定義書<br>＝物理モデル]-->|変換ルールを定義|統合データ</pre>

<p>バリューチェーン（Value Chain &#x2F; 価値連鎖）から始める理由は大きく３つあります。</p>
<ol>
<li>経営資源の集中<ul>
<li>「自社の活動のどこで価値（利益）が生まれ、どこで無駄（コスト）が発生しているか」を特定することで、データ整備やAI適用の優先順位を明確にし、投資を最も効果的な場所に集中できます。</li>
</ul>
</li>
<li>データ基盤構築の羅針盤<ul>
<li>価値の源泉が分かれば、どのデータを、どの粒度で管理すべきかが明確になります。これにより、無駄なデータ統合を避け、議論が発散した際の拠り所となります。</li>
</ul>
</li>
<li>AIのコンテキストとしての観点：Markdownによるデータ構造の表現<ul>
<li>GeminiのようなAIにデータモデリングを依頼する際、最も効果的なのは、企業のデータ資産の全体像を<strong>人間とAIの両方が理解できる形式</strong>でコンテキストとして与えることです。その最適な方法の1つが、<strong>VSCodeのフォルダ構造を模したリポジトリの階層構造でデータを整理する</strong>ことです。</li>
</ul>
</li>
</ol>
<p>例えば、以下のようにデータ資産をフォルダーの階層構造を利用して整理した例です。このように、階層構造含めコンテキストとしてAIに与えることで、AIは「どのデータがどこにあり、どのような関係性を持つ可能性があるか」を正確に把握し、より精度の高いER図や統合ロジックを生成してくれます。</p>
<figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">.</span><br><span class="line">├── 研究開発/</span><br><span class="line">│   ├── R&amp;D_projects.csv <span class="comment"># 研究開発プロジェクト情報</span></span><br><span class="line">│   └── patent_data.csv  <span class="comment"># 特許情報</span></span><br><span class="line">├── 調達/</span><br><span class="line">│   ├── supplier_contracts.csv      <span class="comment"># サプライヤー契約情報</span></span><br><span class="line">│   └── raw_materials_inventory.csv <span class="comment"># 原材料在庫情報</span></span><br><span class="line">├── 製造/</span><br><span class="line">│   ├── production_logs.csv         <span class="comment"># 製造ログ</span></span><br><span class="line">│   └── quality_control_results.csv <span class="comment"># 品質管理結果</span></span><br><span class="line">├── 物流/</span><br><span class="line">│   ├── shipping_records.csv    <span class="comment"># 出荷記録</span></span><br><span class="line">│   └── warehouse_inventory.csv <span class="comment"># 倉庫在庫情報</span></span><br><span class="line">├── 販売/</span><br><span class="line">│   ├── sales_orders.csv      <span class="comment"># 受注データ</span></span><br><span class="line">│   └── customer_feedback.csv <span class="comment"># 顧客フィードバック</span></span><br><span class="line">└── サービス/</span><br><span class="line">    ├── support_tickets.csv     <span class="comment"># サポートチケット</span></span><br><span class="line">    └── maintenance_records.csv <span class="comment"># メンテナンス記録</span></span><br></pre></td></tr></table></figure>

<p>さて、この概念モデルはどうやって物理モデルに展開すればいいか、まず、従来の<strong>セマンティックモデル</strong>（またはセマンティックレイヤー）の関係への理解を深めてから、具体的な手順を紹介したいと思います。</p>
<h3 id="セマンティックモデルとの関係">セマンティックモデルとの関係</h3><p>セマンティックモデルは、物理的なデータ構造（テーブルやカラム）と、ビジネスユーザーが理解できる言葉（指標や属性）との間の「翻訳層」の役割を果たします。</p>
<ul>
<li><strong>バリューチェーン（概念）との接続</strong>: バリューチェーンで定義した「どこで価値が生まれるか」という概念は、セマンティックモデルにおける「メトリクス（例: 売上高、顧客獲得数）」として具体的に定義されます。</li>
<li><strong>データ（物理）の抽象化</strong>: ユーザーは <code>SUM(sales.amount)</code> のようなSQLを書く代わりに、「総売上」というビジネス用語でデータにアクセスできるようになります。</li>
</ul>
<p>AI、特にLLMは、このセマンティックレイヤーを解釈することで、より自然言語に近い形でデータに関する問いに答えたり、分析したりすることが可能になります。つまり、バリューチェーンからセマンティックモデルを定義することは、AIがビジネスコンテキストを理解するための「共通言語」を与えることに他なりません。</p>
<p>以下は、dbtなどのツールで定義されるセマンティックモデルの概念をMermaidで表現した例です。</p>
<pre class="mermaid" data-mermaid="25722fa895b8b4e2d0d1a853e588b4426f128f0f6e3435b59b27030a43cdca2a">graph TD
    subgraph "Business User View"
        Metrics[売上高<br>顧客単価]
        Dimensions[製品カテゴリ<br>地域]
    end

    subgraph "Semantic Layer (dbt, LookML, etc.)"
        direction LR
        Def_Metrics["メトリクス定義<br>SUM(f.amount) as total_revenue"]
        Def_Dimensions["ディメンション定義<br>c.category as product_category"]
    end

    subgraph "Data Warehouse (Physical Model)"
        direction LR
        fct_orders(fact_orders)
        dim_customers(dim_customers)
    end

    Metrics & Dimensions -- "Uses" --> Def_Metrics & Def_Dimensions
    Def_Metrics & Def_Dimensions -- "Abstracts" --> fct_orders & dim_customers</pre>

<h2 id="AI-readyデータとは">AI-readyデータとは</h2><p>AI-ready データとは、AIモデルが学習や分析をスムーズに、かつ正確に行うために、以下の状態に「整えられている」高品質なデータを指します。先ほど議論してきた概念モデルやバリューチェーンは<strong>関連性</strong>という性質に関連しております。<strong>正確性</strong>や<strong>一貫性</strong>に関しては、AIを利用することで、従来の属人のやり方より網羅的に課題をあぶり出すことができます（詳しい手順は次の章）。</p>
<ul>
<li>正確性:<ul>
<li>データに間違いや入力ミスがないこと。</li>
</ul>
</li>
<li>一貫性:<ul>
<li>例えば、同じ意味のデータが「東京」「トウキョウ」「TKO」のようにバラバラに書かれておらず、統一されていること。</li>
<li>欠損値が適切に処理されており、その扱いに関するルール（例: NULLで統一）が明確であること。</li>
</ul>
</li>
<li>関連性（目的に合っているか）:<ul>
<li>AIが解決したい課題（例えば、商品の売上予測）に対して、本当に必要な情報が含まれていること。</li>
</ul>
</li>
<li>代表性:<ul>
<li>データが現実世界の分布を適切に代表していること。</li>
</ul>
</li>
</ul>
<p>データプロファイリングは、Pythonのdata_profilingライブラリを使うと一括で可視化・分析できます。AIの分析結果との整合性を比較するために生成しておくと便利です。</p>
<iframe src="https://drive.google.com/file/d/1WDtI6QyD1dFHrz3Dv1gmkUREmbLT1K8e/preview" width="640" height="480"></iframe>

<h2 id="AI-Readyのためのデータモデリング">AI-Readyのためのデータモデリング</h2><p>さて、いよいよ手を動かす時間です！</p>
<p>従来のデータモデリング（概念→論理→物理）は、ウォーターフォール的に進められることが多く、数ヶ月かけて作成した物理モデルが、いざ開発段階になると業務実態と合わなくなる、といった手戻りが多発しました。</p>
<p>AI時代では、AIを「高速な壁打ち相手」として活用し、このプロセスをアジャイルに（反復的に）進めることができます。以下の各例はGEMINI‐CLIを利用したときの例です。</p>
<h3 id="ステップ1-AIとバリューチェーン（概念）のドラフトを作成">ステップ1: AIとバリューチェーン（概念）のドラフトを作成</h3><p>まず、業務担当者へのインタビューメモや、既存の業務フロー図、中期経営計画などのドキュメントをGeminiにインプットし、バリューチェーンの「たたき台」を作成させます。</p>
<p><strong>プロンプト例:</strong></p>
<figure class="highlight txt"><table><tr><td class="code"><pre><span class="line">あなたは製造業のコンサルタントです。</span><br><span class="line">以下のインタビューメモに基づき、</span><br><span class="line">この企業のバリューチェーン（主活動）を『研究開発』『調達』『製造』『物流』『販売』『サービス』の観点で整理し、</span><br><span class="line">それぞれの活動で重要と思われるKPI（候補）を挙げてください。</span><br></pre></td></tr></table></figure>

<p><strong>▼アウトプット例</strong></p>
<pre class="mermaid" data-mermaid="499122dbcee8ac92bc0f145d06c0e8dcd906ba6b84a2b70ee431ebe09f50ed24">graph TD
    subgraph 主活動
        A[研究開発] --> B[調達]
        B --> C[製造]
        C --> D[物流]
        D --> E[販売]
        E --> F[サービス]
    end
    subgraph 主要KPI
        A --- KPI_A[新製品開発サイクル<br>研究開発費率]
        B --- KPI_B[サプライヤー納期遵守率<br>原材料コスト削減率]
        C --- KPI_C[生産リードタイム<br>不良品率]
        D --- KPI_D[在庫回転率<br>配送コスト]
        E --- KPI_E[顧客獲得単価<br>売上高成長率]
        F --- KPI_F[顧客満足度<br>初回コール解決率]
    end</pre>

<h3 id="ステップ2-AIとデータプロファイリング（物理）を実施">ステップ2: AIとデータプロファイリング（物理）を実施</h3><p>次に、ステップ1で特定したバリューチェーンの「価値の源泉」に関わる既存データ（例：「販売」領域のデータ）をプロファイリングし、前述のMarkdown形式でAIにインプットして、セマンティクスの問題をあぶり出します。</p>
<p><strong>プロンプト例:</strong></p>
<figure class="highlight txt"><table><tr><td class="code"><pre><span class="line">あなたはデータアーキテクトです。</span><br><span class="line">以下のMarkdownで表現されたデータ資産マップに基づき、</span><br><span class="line">企業全体のバリューチェーンを俯瞰できるような統合データモデルのER図をMermaidで作成してください。</span><br><span class="line">各エンティティ間の関係性（1対多、多対多など）も定義してください。</span><br></pre></td></tr></table></figure>

<p><strong>▼レスポンス例</strong></p>
<pre class="mermaid" data-mermaid="28005f208bd1e41fef64c12298f30e4b26542e5c8cc1d80356913084a3349b5e">erDiagram
    SUPPLIERS {
        string supplier_id PK
        string supplier_name
    }
    RAW_MATERIALS {
        string material_id PK
        string material_name
        string supplier_id FK
    }
    PRODUCTS {
        string product_id PK
        string product_name
    }
    PRODUCTION_LOGS {
        string log_id PK
        string product_id FK
        date production_date
        int quantity
    }
    CUSTOMERS {
        string customer_id PK
        string customer_name
    }
    SALES_ORDERS {
        string order_id PK
        string customer_id FK
        date order_date
    }
    ORDER_ITEMS {
        string order_id FK
        string product_id FK
        int quantity
    }
    SUPPORT_TICKETS {
        string ticket_id PK
        string customer_id FK
        string product_id FK
        string issue_details
    }

    SUPPLIERS ||--|{ RAW_MATERIALS : "supplies"
    PRODUCTS ||--o{ PRODUCTION_LOGS : "is_produced"
    CUSTOMERS ||--o{ SALES_ORDERS : "places"
    SALES_ORDERS ||--|{ ORDER_ITEMS : "contains"
    PRODUCTS ||--|{ ORDER_ITEMS : "details"
    CUSTOMERS ||--o{ SUPPORT_TICKETS : "raises"
    PRODUCTS ||--o{ SUPPORT_TICKETS : "concerns"</pre>

<h3 id="ステップ3-AIと論理モデルをKPIに向けて改善">ステップ3: AIと論理モデルをKPIに向けて改善</h3><p>ステップ2で明らかになったセマンティクスの不一致を解決すると同時に、<strong>ステップ1で定義したKPI（例：顧客獲得単価、顧客満足度）を計測・分析できるデータモデル</strong>へと進化させます。</p>
<p><strong>プロンプト例（初回）:</strong></p>
<figure class="highlight txt"><table><tr><td class="code"><pre><span class="line">ありがとうございます。ステップ2の分析に基づき、「見込み客」から「取引先」への転換プロセスを追跡できるようにモデルを設計してください。</span><br><span class="line">これにより、ステップ1で定義した**顧客獲得単価(CAC)の算出**が可能になるはずです。</span><br><span class="line">統合顧客マスタの論理モデル（ER図）をMermaid形式で提案してください。</span><br></pre></td></tr></table></figure>

<p><strong>▼レスポンス例(初回）</strong></p>
<pre class="mermaid" data-mermaid="7e2e6fe210baf1031130f9ea580e0152e2a9f81c27391e337403f164c3305e9f">erDiagram
    LEADS {
        string lead_id PK
        string name
        string status "例: 'New', 'Contacted', 'Qualified'"
        date acquisition_date "獲得日"
        string source "獲得ソース"
    }
    CLIENTS {
        string client_code PK
        string name
        string lead_id FK "紐づく見込み客"
    }
    LEADS ||--|{ CLIENTS : "converts to"</pre>

<p><em>AIはCAC算出を意識して、<code>acquisition_date</code> や <code>source</code> といった属性を追加してくれるかもしれません。</em></p>
<p><strong>プロンプト例（修正指示）:</strong></p>
<figure class="highlight txt"><table><tr><td class="code"><pre><span class="line">ありがとうございます。顧客獲得の分析ができそうです。</span><br><span class="line">次に、**顧客満足度やLTV（顧客生涯価値）**の分析も視野に入れたいです。</span><br><span class="line">先ほどのモデルに、販売後の「注文」と「サポート」の概念を追加してください。</span><br><span class="line">どの注文に関する問い合わせかを追跡できるようにすることで、製品やサービスの問題点を特定し、顧客満足度の改善に繋げたいです。</span><br></pre></td></tr></table></figure>

<p>このように、AIが生成したモデルを元に、ビジネスKPIを達成するための改善指示を繰り返すことで、モデルをアジャイルに進化させることができます。最終的に、記事の前半で示したような、バリューチェーン全体を俯瞰する包括的なER図へと発展させていきます。</p>
<h3 id="ステップ4-KPIに向けてデータ統合ロジックを生成">ステップ4: KPIに向けてデータ統合ロジックを生成</h3><p>論理モデルが固まったら、次はそのモデルに元データを投入するための具体的なデータ統合（ETL&#x2F;ELT）ロジックをAIに作成させます。</p>
<p><strong>プロンプト例:</strong></p>
<figure class="highlight txt"><table><tr><td class="code"><pre><span class="line">ありがとうございます。論理モデルがFIXしました。</span><br><span class="line">ステップ2で提示した2つのCSV (`sales/customers.csv` と `finance/clients.csv`) から、</span><br><span class="line">先ほど作成した `LEADS` テーブルと `CLIENTS` テーブルにデータを統合するためのSQL（INSERT文）を生成してください。</span><br><span class="line">その際、以下の要件を満たしてください。</span><br><span class="line">- `sales/customers.csv` のうち、statusが「契約済」の顧客を `CLIENTS` テーブルに登録する。</span><br><span class="line">- それ以外のステータスの顧客は `LEADS` テーブルに登録する。</span><br><span class="line">- `finance/clients.csv` の `client_name` の表記揺れ（例：「株式会社A」と「A株式会社」）を吸収して、`CLIENTS` テーブルの名寄せを行う。</span><br></pre></td></tr></table></figure>

<p><strong>▼レスポンス例</strong></p>
<figure class="highlight sql"><table><tr><td class="code"><pre><span class="line"><span class="comment">-- sales/customers.csv から LEADS テーブルへのデータ投入</span></span><br><span class="line"><span class="keyword">INSERT INTO</span> LEADS (lead_id, name, status)</span><br><span class="line"><span class="keyword">SELECT</span></span><br><span class="line">    customer_id,</span><br><span class="line">    name,</span><br><span class="line">    status</span><br><span class="line"><span class="keyword">FROM</span></span><br><span class="line">    sales_customers</span><br><span class="line"><span class="keyword">WHERE</span></span><br><span class="line">    status <span class="operator">!=</span> <span class="string">&#x27;契約済&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">-- sales/customers.csv と finance/clients.csv から CLIENTS テーブルへのデータ投入と名寄せ</span></span><br><span class="line"><span class="keyword">INSERT INTO</span> CLIENTS (client_code, name, lead_id)</span><br><span class="line"><span class="keyword">SELECT</span></span><br><span class="line">    <span class="comment">-- 取引先コードは経理のものを正とする</span></span><br><span class="line">    fc.client_code,</span><br><span class="line">    <span class="comment">-- 名前の表記揺れを吸収 (例として `REPLACE` を使用)</span></span><br><span class="line">    REPLACE(REPLACE(sc.name, <span class="string">&#x27;株式会社&#x27;</span>, <span class="string">&#x27;&#x27;</span>), <span class="string">&#x27;商事&#x27;</span>, <span class="string">&#x27;&#x27;</span>) <span class="keyword">AS</span> unified_name,</span><br><span class="line">    sc.customer_id</span><br><span class="line"><span class="keyword">FROM</span></span><br><span class="line">    sales_customers sc</span><br><span class="line"><span class="keyword">JOIN</span></span><br><span class="line">    finance_clients fc</span><br><span class="line"><span class="keyword">ON</span></span><br><span class="line">    REPLACE(REPLACE(sc.name, <span class="string">&#x27;株式会社&#x27;</span>, <span class="string">&#x27;&#x27;</span>), <span class="string">&#x27;商事&#x27;</span>, <span class="string">&#x27;&#x27;</span>) <span class="operator">=</span> REPLACE(REPLACE(fc.client_name, <span class="string">&#x27;株式会社&#x27;</span>, <span class="string">&#x27;&#x27;</span>), <span class="string">&#x27;商事&#x27;</span>, <span class="string">&#x27;&#x27;</span>)</span><br><span class="line"><span class="keyword">WHERE</span></span><br><span class="line">    sc.status <span class="operator">=</span> <span class="string">&#x27;契約済&#x27;</span>;</span><br><span class="line"></span><br></pre></td></tr></table></figure>

<h3 id="ステップ5-AIと物理モデル（DDL）とドキュメントを生成">ステップ5: AIと物理モデル（DDL）とドキュメントを生成</h3><p>論理モデルが固まったら、最後のステップとして、AIに物理的な実装成果物であるDDL（Data Definition Language）やテーブル定義書の草案を作成させます。これにより、エンジニアは単純なコーディング作業から解放され、より複雑な実装に集中できます。</p>
<p><strong>プロンプト例:</strong></p>
<figure class="highlight txt"><table><tr><td class="code"><pre><span class="line">ありがとうございます。最終的な論理モデルが完成しました。</span><br><span class="line">このER図に基づき、PostgreSQL互換のDDL（CREATE TABLE文）を生成してください。</span><br><span class="line">各カラムには、その内容がわかるようにコメントを追加してください。</span><br><span class="line">また、各テーブルの目的をまとめたMarkdown形式のテーブル定義書も作成してください。</span><br></pre></td></tr></table></figure>

<p><strong>▼AIによるアウトプット例</strong></p>
<p><strong>1. DDL (SQL)</strong></p>
<figure class="highlight sql"><table><tr><td class="code"><pre><span class="line"><span class="comment">-- 見込み客テーブル</span></span><br><span class="line"><span class="keyword">CREATE TABLE</span> LEADS (</span><br><span class="line">    lead_id <span class="type">VARCHAR</span>(<span class="number">255</span>) <span class="keyword">PRIMARY KEY</span>, <span class="comment">-- 見込み客ID</span></span><br><span class="line">    name <span class="type">VARCHAR</span>(<span class="number">255</span>), <span class="comment">-- 会社名または担当者名</span></span><br><span class="line">    status <span class="type">VARCHAR</span>(<span class="number">50</span>), <span class="comment">-- ステータス (例: &#x27;New&#x27;, &#x27;Contacted&#x27;, &#x27;Qualified&#x27;)</span></span><br><span class="line">    acquisition_date <span class="type">DATE</span>, <span class="comment">-- 獲得日</span></span><br><span class="line">    source <span class="type">VARCHAR</span>(<span class="number">100</span>) <span class="comment">-- 獲得ソース (例: &#x27;Web&#x27;, &#x27;Event&#x27;)</span></span><br><span class="line">);</span><br><span class="line"></span><br><span class="line"><span class="comment">-- 取引先テーブル</span></span><br><span class="line"><span class="keyword">CREATE TABLE</span> CLIENTS (</span><br><span class="line">    client_code <span class="type">VARCHAR</span>(<span class="number">255</span>) <span class="keyword">PRIMARY KEY</span>, <span class="comment">-- 取引先コード</span></span><br><span class="line">    name <span class="type">VARCHAR</span>(<span class="number">255</span>), <span class="comment">-- 取引先名</span></span><br><span class="line">    lead_id <span class="type">VARCHAR</span>(<span class="number">255</span>) <span class="keyword">REFERENCES</span> LEADS(lead_id) <span class="comment">-- 紐づく見込み客ID</span></span><br><span class="line">);</span><br><span class="line"></span><br><span class="line"><span class="comment">-- 注文テーブル</span></span><br><span class="line"><span class="keyword">CREATE TABLE</span> SALES_ORDERS (</span><br><span class="line">    order_id <span class="type">VARCHAR</span>(<span class="number">255</span>) <span class="keyword">PRIMARY KEY</span>, <span class="comment">-- 注文ID</span></span><br><span class="line">    client_code <span class="type">VARCHAR</span>(<span class="number">255</span>) <span class="keyword">REFERENCES</span> CLIENTS(client_code), <span class="comment">-- 取引先コード</span></span><br><span class="line">    order_date <span class="type">DATE</span>, <span class="comment">-- 注文日</span></span><br><span class="line">    status <span class="type">VARCHAR</span>(<span class="number">50</span>) <span class="comment">-- 注文ステータス (例: &#x27;受注&#x27;, &#x27;出荷済&#x27;, &#x27;キャンセル&#x27;)</span></span><br><span class="line">);</span><br><span class="line"></span><br><span class="line"><span class="comment">-- サポートチケットテーブル</span></span><br><span class="line"><span class="keyword">CREATE TABLE</span> SUPPORT_TICKETS (</span><br><span class="line">    ticket_id <span class="type">VARCHAR</span>(<span class="number">255</span>) <span class="keyword">PRIMARY KEY</span>, <span class="comment">-- チケットID</span></span><br><span class="line">    client_code <span class="type">VARCHAR</span>(<span class="number">255</span>) <span class="keyword">REFERENCES</span> CLIENTS(client_code), <span class="comment">-- 取引先コード</span></span><br><span class="line">    order_id <span class="type">VARCHAR</span>(<span class="number">255</span>) <span class="keyword">REFERENCES</span> SALES_ORDERS(order_id), <span class="comment">-- 関連する注文ID</span></span><br><span class="line">    issue_details TEXT <span class="comment">-- 問い合わせ内容</span></span><br><span class="line">);</span><br></pre></td></tr></table></figure>

<p><strong>2. テーブル定義書 (Markdown)</strong></p>
<div class="scroll"><table>
<thead>
<tr>
<th align="left">テーブル名</th>
<th align="left">物理名</th>
<th align="left">目的</th>
</tr>
</thead>
<tbody><tr>
<td align="left">見込み客</td>
<td align="left">LEADS</td>
<td align="left">営業活動の対象となる潜在顧客を管理する。</td>
</tr>
<tr>
<td align="left">取引先</td>
<td align="left">CLIENTS</td>
<td align="left">請求実績のある契約済み顧客を管理する。</td>
</tr>
<tr>
<td align="left">注文</td>
<td align="left">SALES_ORDERS</td>
<td align="left">取引先からの製品やサービスの注文情報を管理する。</td>
</tr>
<tr>
<td align="left">サポートチケット</td>
<td align="left">SUPPORT_TICKETS</td>
<td align="left">顧客からの問い合わせやサポート依頼を管理する。</td>
</tr>
</tbody></table></div>
<p>このように、最終的な物理モデルの生成までAIを活用することで、データモデリングの全工程を高速化し、一貫性を保つことができます。</p>
<h2 id="おわりに">おわりに</h2><p>LLM（AI）が登場したからといって、データモデリングの重要性がなくなるわけではありません。むしろ、AIがデータの「意味」を理解するために、セマンティクスを定義するデータモデリングの重要性は増しています。</p>
<p>また、AIはデータモデリングという従来は<strong>職人技であったプロセスを民主化</strong>し、高速化してくれる強力なパートナーでもあります。具体的な活用機会として、以下の各レイヤにあるのではないかと考えます。</p>
<div class="scroll"><table>
<thead>
<tr>
<th align="left">レイヤー</th>
<th align="left">役割</th>
<th align="left">AIの活用機会</th>
</tr>
</thead>
<tbody><tr>
<td align="left">メタデータ</td>
<td align="left">構造</td>
<td align="left">分類と検証</td>
</tr>
<tr>
<td align="left">セマンティクス</td>
<td align="left">意味</td>
<td align="left">自然言語によるドキュメンテーション</td>
</tr>
<tr>
<td align="left">リネージ</td>
<td align="left">コンテキスト</td>
<td align="left">影響分析</td>
</tr>
<tr>
<td align="left">ルール</td>
<td align="left">ガバナンス</td>
<td align="left">ポリシーの自動チェック</td>
</tr>
<tr>
<td align="left">AI</td>
<td align="left">インテリジェンス</td>
<td align="left">生成と推論</td>
</tr>
</tbody></table></div>
<p>このようにAIと協働しながら、<strong>トップダウン（概念モデル）でビジネス価値を定義し、ボトムアップ（既存データ）の現状をMarkdownで構造化してAIに提示する</strong>、 という両輪を回すことが、手戻りのないデータ基盤を構築し、AI活用の成功を掴むための最短ルートとなるでしょう。</p>
<h2 id="参考">参考</h2><ul>
<li>MODERN DATA MODELING: THE QUIET REVOLUTION POWERING THE AI-DRIVEN ENTERPRISE</li>
<li>SNOWFLAKE、SALESFORCE、DBT LABSなどが、オープン セマンティック インターチェンジ共同構想によってAIのためのデータ活用準備を革新</li>
<li>HOW UNIVERSAL DATA MODELS SUPPORT BUSINESS-ORIENTED DATA MODELING</li>
<li>アジャイルデータモデリング 組織にデータ分析を広めるためのテーブル設計ガイド (KS情報科学専門書)</li>
</ul>
]]></content>
    <summary type="html">多くの企業がAI活用を模索する一方で、「AIに何を食べさせればよいか分からない」「データが散在・サイロ化していて使えない」といった課題に直面しています。フューチャー恒例の秋のブログ週間を機に、今回はAI（特にGEMINI-CLI）を利用して、どのようにAIと協働してデータモデリングできるか、直近のデータモデリングのトレンドを調べました。</summary>
    <category term="DataEngineering" scheme="https://future-architect.github.io/categories/DataEngineering/"/>
    <category term="Gemini" scheme="https://future-architect.github.io/tags/Gemini/"/>
    <category term="GoogleCloud" scheme="https://future-architect.github.io/tags/GoogleCloud/"/>
    <category term="dbt" scheme="https://future-architect.github.io/tags/dbt/"/>
    <category term="pandas" scheme="https://future-architect.github.io/tags/pandas/"/>
    <category term="データモデル" scheme="https://future-architect.github.io/tags/%E3%83%87%E3%83%BC%E3%82%BF%E3%83%A2%E3%83%87%E3%83%AB/"/>
  </entry>
  <entry>
    <title>Rustベースのdbt fusion engineを使ってみた！</title>
    <link href="https://future-architect.github.io/articles/20250828a/"/>
    <id>https://future-architect.github.io/articles/20250828a/</id>
    <published>2025-08-27T15:00:00.000Z</published>
    <updated>2025-08-27T15:00:00.000Z</updated>
    <author><name>大前七奈</name></author>
    <content type="html"><![CDATA[<img fetchpriority="high" src="/images/2025/20250828a/image.png" alt="image.png" width="600" height="339">

<p>夏の自由研究2025ブログ連載の3日目です。</p>
<p>こんにちは！Energy Transformation Groupの大前七奈です。</p>
<p>dbtは、データエンジニアリングの現場に革新をもたらしましたが、プロジェクトが大規模になるにつれて、いくつかの課題も浮き彫りになってきました。</p>
<p>本記事では、その課題を解決するために開発された次世代のエンジン「dbt Fusion Engine」について、実際に試してみた所感を交えながら、その凄さや移行方法、そして今後の展望について詳しくお話ししたいと思います。</p>
<h2 id="改めてdbtすごいところ">改めてdbtすごいところ</h2><p>dbt（Data Build Tool）は、データエンジニアリング界隈に革命をもたらしたELT（Extract, Load, Transform）ツールです。Gitバージョン管理システムで、SQLでデータ変換を管理でき、さらにデータ変換のステップを複数の小さなSQLファイルに分割して管理することで、コードの再利用が容易になり、メンテンス性も向上します。</p>
<p>また、データ品質を保証するためのテスト機能と、データのドキュメントを自動生成する機能が組み込まれています。dbtは、データパイプラインをより効率的に構築・管理できるようになりました。</p>
<h2 id="既存のdbt-coreの問題点">既存のdbt-coreの問題点</h2><p>しかし、プロジェクトが大規模になるにつれて、dbt開発チームには以下のいくつかの課題に直面しました。</p>
<ul>
<li>遅いコンパイル時間<ul>
<li>開発当初に完了時間が1分のバッチジョブが、データモデルの数とデータ自体の増加により10分までに増加しました</li>
</ul>
</li>
<li>Pythonの依存関係の競合<ul>
<li><code>dbt-core</code> v1.8.xが<code>protobuf</code>&lt;5.0.0を要求するのに対して、データサイエンス系の<code>scikit-learn</code>や<code>tensorflow</code>の最新バージョンなら<code>protobuf</code>&gt;&#x3D;5.0.0を要求する場合</li>
</ul>
</li>
<li>冗長なウェアハウス実行<ul>
<li>デバックはdbt実行してデータウェアハウスにアクセスする手段しかない</li>
</ul>
</li>
<li>限定的なIDEサポート<ul>
<li>リアルタイムかつローカルなバリエーションができない</li>
</ul>
</li>
</ul>
<h2 id="dbt-fusion-engineが起こす革命">dbt fusion engineが起こす革命</h2><p>以上の課題を解消するために、dbt LabsがRustで、<code>dbt-core</code>を書き直して、2025&#x2F;5&#x2F;28に、dbt fusion engineをリリースしました。dbt Fusionのリリースと同時に、データエンジニアが待ちに待ったVS Code拡張機能も新たに導入されました。</p>
<p>Pythonベースのdbt-coreと異なり、SQLをデータウェアハウスに送信する前に、dbt Fusion EngineがSQLをローカルでパースし、コードを分析・検証し、リアルタイムにエラーを教えてくれます。データウェアハウスにアクセスすることなく、つまり、時間とコストの両方を節約できます。</p>
<ul>
<li>入力中のライブエラー検出</li>
<li>データモデルを理解するスマートなオートコンプリート</li>
<li>カラムやモデルへのGo-to-definition機能</li>
<li>クエリを実行せずにインラインCTEをプレビュー</li>
<li>ホバーでモデルのメタデータを表示 などなど</li>
</ul>
<p>詳しくは、dbt VS Code拡張機能の紹介動画がありますので、ご参考ください。</p>
<h2 id="移行方法">移行方法</h2><h3 id="０．利用するバージョン">０．利用するバージョン</h3><ul>
<li>dbt fusion engine beta: 2.0.0-preview.6 (2025&#x2F;8&#x2F;28時点)</li>
<li>dbt-adpater: 1.16.3</li>
<li>dbt-bigquery: 1.8.1</li>
</ul>
<h3 id="１．インストール（MacOs-Linuxの場合）">１．インストール（MacOs, Linuxの場合）</h3><p>MacOs, Linuxの場合は、dbt VS Code拡張機能をインストールすると、Fusionをインストールするポップアップが出てくるので、「はい」とクリックすると、自動的にFusionがインストールされます。</p>
<p>詳しい手順は、公式サイト（dbt VS Code拡張をインストールする）をご参考ください。</p>
<div class="note-container note-info note-has-title"><div class="note-title"><span class="note-icon"></span>Windowsにおけるインストール</div><div class="note-body">

<p>Windowsの場合は、VS Code拡張機能を経由してFusionのインストールできなかったため、PowerShellで以下のコマンドをたたき、dbt.exeのパスを通すと、上記と同じようにdbt fusionを利用できました。</p>
<figure class="highlight ps"><table><tr><td class="code"><pre><span class="line"><span class="built_in">irm</span> https://public.cdn.getdbt.com/fs/install/install.ps1 | <span class="built_in">iex</span></span><br></pre></td></tr></table></figure>

</div></div>

<h3 id="２．インストール後の対応">２．インストール後の対応</h3><p><code>dbt-core</code>からの移行手順もとても簡単です。以下のコマンドをただくと、画像のとおり、いくつかの質問に答えると、自動的に<code>dbt-autofix</code>まで適用して移行のためのファイルを書き換えてくれます。</p>
<figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">dbt init --fusion-upgrade</span><br><span class="line"></span><br><span class="line">&gt;&gt; Do you have an existing dbt platform account(fka dbt Cloud)?</span><br><span class="line"></span><br><span class="line">&gt;&gt; Do you have an existing dbt project?</span><br><span class="line"></span><br><span class="line">&gt;&gt; Ready to run a dbtf parse? Press [ENTER] to <span class="built_in">continue</span></span><br></pre></td></tr></table></figure>

<p>実際の移行時のログ：</p>
<img src="/images/2025/20250828a/image_2.png" alt="image.png" width="800" height="577" loading="lazy">

<h2 id="dbt-fusion-engine使ってみた所感">dbt fusion engine使ってみた所感</h2><h3 id="良かった点">良かった点</h3><p>目論見通り体感できるほど高速化しました。ただし、最大30倍高速になるという謳い文句ほどではありませんでした。</p>
<p>例えば、過去に3分実行時間が必要なパイプラインがおおよそ1分で終わりました。</p>
<div class="note-container note-warn note-has-title"><div class="note-title"><span class="note-icon"></span>よくよく考えてみると、dbt fusionはあくまでSQL実行計画をしてくれるツールであり、30倍早くならなかった理由は以下の様に考えられます。</div><div class="note-body">

<ul>
<li>そもそもSQL自体の最適化が徹底されていない</li>
<li>そもそもSQLの依存関係が複雑すぎる</li>
<li>marcos テンプレートの多用により解析時間がかかる</li>
</ul>
</div></div>

<h3 id="気になった点">気になった点</h3><ul>
<li><code>dbt-autofix</code>で差分が大量に発生しました<ul>
<li>事前にブランチを切って移行による変更点を分けると良いです</li>
<li>特に <code>&#123;&#123; ref("table_name") &#125;&#125; -&gt; &#123;&#123; ref("source", "table_name") &#125;&#125;</code> などJinjaテンプレートへの変更の割合が多かったです</li>
</ul>
</li>
<li><code>dbt-autofix</code>後にSyntaxの違いによる手動変更が必要です</li>
<li>dbt hubの外部パッケージ未対応です<ul>
<li>プロジェクト内でカラムの<code>description</code>が伝播されるように<code>dbt-osmosis</code>を利用していますが、fusionの現バージョンで未対応のため、一旦<code>profiles.yml</code>内の関連記述をコメントアウトしてからようやく<code>dbt run</code>を実行できました</li>
</ul>
</li>
<li>バグがまだまだあります<ul>
<li>たとえば、データウェアハウス内にデータがない場合、今までdbt-coreではエラーが出なかったのに、Fusionで同じ実行すると、<code>Parquet ArrowWriter Error</code>が出ました</li>
</ul>
</li>
</ul>
<div class="note-container note-warn"><span class="note-icon"></span><div>

<p>上記のエラーが公式GithubのIssueとして発行されておりました。もし謎のバグに出合った際、Issue一覧から類似するバグがないか、一度調べてみるとよいです。</p>
</div></div>

<h2 id="いつ正式運用できるか？">いつ正式運用できるか？</h2><p>以下の公式ドキュメントによりますと、以下の何点か未対応だそうです。ただ、具体的な完了時期が言及されていません。</p>
<ul>
<li>マテリアライズ機能を利用しているモデル</li>
<li>Fusionのロギングシステムは、現在不安定で不完全です</li>
<li>dbtプラットフォームの補完的な機能（モデルレベルの通知、高度なCI、およびセマンティックレイヤーなど）に依存するワークフロー</li>
</ul>
<p>参考：GAへの道</p>
<p>以上で、dbt fusion engineの使ってみた所感でした！個人的にマテリアライズ機能やCI機能が揃ったら導入をしてもよいかと思いました。しばらく公式サイトのIssueの消化具合を見つつ、導入時期を判断していきたいです。</p>
]]></content>
    <summary type="html">dbtは、データエンジニアリングの現場に革新をもたらしましたが、プロジェクトが大規模になるにつれて、いくつかの課題も浮き彫りになってきました。その課題を解決するために開発された次世代のエンジン「dbt Fusion Engine」について...</summary>
    <category term="DataEngineering" scheme="https://future-architect.github.io/categories/DataEngineering/"/>
    <category term="Rust" scheme="https://future-architect.github.io/tags/Rust/"/>
    <category term="dbt" scheme="https://future-architect.github.io/tags/dbt/"/>
  </entry>
  <entry>
    <title>はじめてGlue Python Shell Jobを使う時のつまづきポイント集</title>
    <link href="https://future-architect.github.io/articles/20250822a/"/>
    <id>https://future-architect.github.io/articles/20250822a/</id>
    <published>2025-08-21T15:00:00.000Z</published>
    <updated>2025-08-21T15:00:00.000Z</updated>
    <author><name>八木雅斗</name></author>
    <content type="html"><![CDATA[<img fetchpriority="high" src="/images/2025/20250822a/top.jpg" alt="" width="700" height="700">

<h2 id="はじめに">はじめに</h2><p>TIGの八木雅斗です。</p>
<p>業務でGlue Python Shell Job（以降、Python Shell）を利用する機会があったのですが、「Lambdaとかだと簡単にできるのに、Python Shellだとできないんか～」とか、ドキュメント上でPySparkジョブ or Python Shellのどちらについて記載しているのか分かりにくかったりと、戸惑うことがありました。それらの悩みポイントをまとめます。</p>
<h2 id="ログ出力先のCloudWatch-Logsのロググループを選択できない">ログ出力先のCloudWatch Logsのロググループを選択できない</h2><p>PySparkジョブでは、<code>--continuous-log-logGroup</code>でロギング先のロググループを選択可能です。</p>
<p>一方、Python Shellでは出力先のロググループを指定できません。</p>
<p>ログは下記のロググループに出力されるよう設定されており、AWSアカウント上のすべてのPython Shellのログが、以下のロググループに出力されるようになっています。</p>
<ul>
<li>標準出力先<ul>
<li><code>/aws-glue/python-jobs/output</code></li>
</ul>
</li>
<li>標準エラー出力先<ul>
<li><code>/aws-glue/python-jobs/error</code></li>
</ul>
</li>
</ul>
<p>そのため、指定したロググループにログを出力したい場合は、Pythonスクリプト上からCloudWatch Logsにログを送信する必要があります。</p>
<p>具体的な対処法としては、boto3を利用した下記のようなカスタムハンドラをルートロガーに設定し、指定したロググループ&#x2F;ログストリームにロギングする等があります。</p>
<figure class="highlight py"><table><tr><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">CloudWatchLogsHandler</span>(logging.Handler):</span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">__init__</span>(<span class="params">self, log_stream_name: <span class="built_in">str</span></span>):</span><br><span class="line">        <span class="built_in">super</span>().__init__()</span><br><span class="line">        <span class="variable language_">self</span>.client = boto3.client(<span class="string">&quot;logs&quot;</span>)</span><br><span class="line">        <span class="comment"># Glueジョブの引数からログ出力先のロググループを取得</span></span><br><span class="line">        args = getResolvedOptions(sys.argv, [<span class="string">&quot;LOG_GROUP_NAME&quot;</span>])</span><br><span class="line">        log_group_name = args[<span class="string">&quot;LOG_GROUP_NAME&quot;</span>]</span><br><span class="line">        <span class="variable language_">self</span>.log_group_name = log_group_name</span><br><span class="line">        <span class="variable language_">self</span>.log_stream_name = log_stream_name</span><br><span class="line">        <span class="comment"># ジョブの実行毎にログストリームを分けるために、インスタンス作成時にログストリームを作成</span></span><br><span class="line">        <span class="variable language_">self</span>.client.create_log_stream(logGroupName=<span class="variable language_">self</span>.log_group_name, logStreamName=<span class="variable language_">self</span>.log_stream_name)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">emit</span>(<span class="params">self, record: logging.LogRecord</span>):</span><br><span class="line">        log_message = <span class="variable language_">self</span>.<span class="built_in">format</span>(record)</span><br><span class="line">        timestamp = <span class="built_in">int</span>(datetime.now().timestamp() * <span class="number">1000</span>)</span><br><span class="line"></span><br><span class="line">        <span class="keyword">try</span>:</span><br><span class="line">            <span class="variable language_">self</span>.client.put_log_events( <span class="comment"># ログを送信</span></span><br><span class="line">                logGroupName=<span class="variable language_">self</span>.log_group_name,</span><br><span class="line">                logStreamName=<span class="variable language_">self</span>.log_stream_name,</span><br><span class="line">                logEvents=[&#123;<span class="string">&quot;timestamp&quot;</span>: timestamp, <span class="string">&quot;message&quot;</span>: log_message&#125;],</span><br><span class="line">            )</span><br><span class="line">        <span class="keyword">except</span> ClientError <span class="keyword">as</span> e:</span><br><span class="line">            <span class="keyword">raise</span> RuntimeError(<span class="string">f&quot;Failed to put log event to CloudWatch Logs.: <span class="subst">&#123;e&#125;</span>&quot;</span>) <span class="keyword">from</span> e</span><br><span class="line"></span><br><span class="line">root_logger = logging.getLogger()</span><br><span class="line">handler = CloudWatchLogsHandler(<span class="string">&quot;test_log_stream&quot;</span>)</span><br><span class="line">root_logger.addHandler(handler)</span><br></pre></td></tr></table></figure>

<p>※Pythonスクリプト上で出力するアプリケーションログでなく、Python Shellが出力するシステムログに関しては引き続き<code>/aws-glue/python-jobs/xxx</code>に出力されます。</p>
<ul>
<li>How to use a CloudWatch custom log group with Python Shell Glue job?</li>
<li>Logging HOWTO</li>
</ul>
<h2 id="自動でメトリクスが取れない">自動でメトリクスが取れない</h2><p>下記の設定もPySpark用の設定であるため、有効化してもメトリクスは取得できません。</p>
<ul>
<li><code>--enable-job-insights</code></li>
<li><code>--enable-observability-metrics</code></li>
</ul>
<p>Google検索すると、Monitoring with AWS Glue job run insightsがヒットして設定できそうに見えますが、よく見ると「Spark and PySpark jobs」配下にあり、PySpark用の設定値であることが分かります（見落としがち）</p>
<p>もしCPUやメモリの利用状況をモニタリングしたい場合は、下記のようにpsutilなどを使ってモニタリングする必要があります。</p>
<figure class="highlight py"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> time</span><br><span class="line"><span class="keyword">import</span> threading</span><br><span class="line"><span class="keyword">import</span> psutil</span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">resource_monitor</span>():</span><br><span class="line">    <span class="string">&quot;&quot;&quot;バックグラウンドで継続的にリソースを監視するタスク&quot;&quot;&quot;</span></span><br><span class="line">    <span class="keyword">while</span> <span class="literal">True</span>:</span><br><span class="line">        cpu = psutil.cpu_percent()</span><br><span class="line">        mem = psutil.virtual_memory().percent</span><br><span class="line">        <span class="built_in">print</span>(<span class="string">f&quot;CPU: <span class="subst">&#123;cpu&#125;</span>% | Memory: <span class="subst">&#123;mem&#125;</span>%&quot;</span>)</span><br><span class="line">        time.sleep(<span class="number">1</span>) <span class="comment"># メインスレッドに影響のない間隔で設定する</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># メインスレッド終了時に一緒に終了させる</span></span><br><span class="line">monitor_thread = threading.Thread(target=resource_monitor, daemon=<span class="literal">True</span>)</span><br><span class="line">monitor_thread.start()</span><br><span class="line"><span class="comment"># ～メインの処理～</span></span><br></pre></td></tr></table></figure>

<h2 id="選択できるPythonのパッチバージョンが古い">選択できるPythonのパッチバージョンが古い</h2><p>2025.08現在、Python Shell ジョブでは、Pythonのバージョンにv3.6またはv3.9を利用できます。</p>
<p>ただ、Python Shellでのv3.9のバージョンは3.9.x系の最新版かと思いきや、2022年1月にリリースされた3.9.10になっています。</p>
<figure class="highlight py"><table><tr><td class="code"><pre><span class="line"><span class="comment"># Python Shellにて</span></span><br><span class="line"><span class="keyword">import</span> sys</span><br><span class="line"><span class="built_in">print</span>(sys.version)</span><br><span class="line"><span class="comment">#=&gt; 3.9.10 (main, Jun 2 2022, 18:40:40) [GCC 7.3.1 20180712 (Red Hat 7.3.1-14)]</span></span><br></pre></td></tr></table></figure>

<p>開発環境やビルド環境においてバージョン違いで動かなくなることを避けるため、<code>3.9.10</code>まで指定する必要がありました。</p>
<ul>
<li>AWS Glue での Python シェルジョブに関するジョブプロパティの設定</li>
</ul>
<h2 id="デフォルトで利用できるライブラリのバージョンが古い">デフォルトで利用できるライブラリのバージョンが古い</h2><p>Python Shellの実行環境では、boto3などのライブラリがサポートされており、追加の設定なしに利用できます。<br>ただし、いずれのライブラリも微妙に古いため、ものによっては使えない機能がある可能性があります。</p>
<figure class="highlight py"><table><tr><td class="code"><pre><span class="line"><span class="comment"># Python Shellにて</span></span><br><span class="line"><span class="keyword">import</span> boto3</span><br><span class="line"><span class="built_in">print</span>(boto3.__version__) <span class="comment"># =&gt;1.21.21</span></span><br></pre></td></tr></table></figure>

<p>もし他のバージョンのライブラリを利用したい場合は、PyPIに登録されているモジュールを追加できる<code>--additional-python-modules</code>オプションを利用することで差し替えることが可能です。</p>
<blockquote>
<p>–additional-python-modules オプションでコンマ区切りの Python モジュールのリストを指定することで、新しいモジュールを追加したり、既存のモジュールのバージョンを変更したりできます。</p>
</blockquote>
<ul>
<li>サポートされている Python シェルジョブのライブラリ</li>
</ul>
<h2 id="存在しない実行パラメータにアクセスするとSystemExitエラーで落ちる">存在しない実行パラメータにアクセスするとSystemExitエラーで落ちる</h2><p>Python ShellではgetResolvedOptionsを利用して実行パラメータにアクセスできます。</p>
<ul>
<li>getResolvedOptions を使用して、パラメータにアクセスする</li>
</ul>
<p>しかし、存在しないパラメータにアクセスしようとすると、Exceptionを継承していないSystemExitのエラーが発生するようになっているため、Exceptionでcatchができません。</p>
<p>具体的には、argparse.ArgumentParserクラスを継承したインスタンスの以下の<code>parse_known_args</code>メソッド内でArgumentErrorが発生し、sys.exit(2)が呼ばれるようになっています。</p>
<ul>
<li>https://github.com/awslabs/aws-glue-libs/blob/9d8293962e6ffc607e5dc328e246f40b24010fa8/awsglue/utils.py#L119</li>
<li>https://github.com/python/cpython/blob/06fc882eac0e59220a7b8b127a1e7babe0055d45/Lib/argparse.py#L1859</li>
</ul>
<p>もしオプションの実行パラメータを設定する場合は、PEP 8で非推奨な方法になりますが、例外の基底クラスであるBaseExceptionでcatchする必要があります（↓例）</p>
<figure class="highlight py"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> sys</span><br><span class="line"><span class="keyword">from</span> awsglue.utils <span class="keyword">import</span> getResolvedOptions</span><br><span class="line"></span><br><span class="line">trace_id: <span class="built_in">str</span></span><br><span class="line"><span class="keyword">try</span>:</span><br><span class="line">    args = getResolvedOptions(sys.argv, [<span class="string">&#x27;TRACE_ID&#x27;</span>])</span><br><span class="line">    trace_id = args[<span class="string">&#x27;TRACE_ID&#x27;</span>]</span><br><span class="line"><span class="keyword">except</span> BaseException:</span><br><span class="line">    trace_id = <span class="string">&quot;unset&quot;</span></span><br></pre></td></tr></table></figure>

<ul>
<li>例外のクラス階層</li>
</ul>
<h3 id="マネジメントコンソール上での変更で想定外の差分が発生する">マネジメントコンソール上での変更で想定外の差分が発生する</h3><p>原因は不明ですが、マネジメントコンソール上から設定変更を行うと、下記の実行パラメータがPython Shellのデフォルト値に勝手に変更されてしまうことがあります。</p>
<ul>
<li>glue_version<ul>
<li>デフォルト値： <code>3.0</code></li>
</ul>
</li>
<li>–enable-job-insights<ul>
<li>デフォルト値： <code>false</code></li>
</ul>
</li>
<li>–enable-observability-metrics<ul>
<li>デフォルト値： <code>false</code></li>
</ul>
</li>
<li>execution_class<ul>
<li>デフォルト値： <code>STANDARD</code></li>
</ul>
</li>
</ul>
<blockquote>
<p>💡利用ツールのバージョン Terraform v1.12.2 &#x2F; AWS provider v6.3.0</p>
</blockquote>
<p>いずれのパラメータもPython Shellの挙動に影響しないですが、Terraformで管理しているとplan実行時に差分が表示されノイズになるので、下記のようにデフォルト値を設定しておくと良いと思います。</p>
<figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">resource <span class="string">&quot;aws_glue_job&quot;</span> <span class="string">&quot;sample&quot;</span> &#123;</span><br><span class="line">  <span class="comment"># ~略~</span></span><br><span class="line">  glue_version = <span class="string">&quot;3.0&quot;</span></span><br><span class="line">  execution_class = <span class="string">&quot;STANDARD&quot;</span></span><br><span class="line">  default_arguments = &#123;</span><br><span class="line">    <span class="string">&quot;--job-language&quot;</span>                 = <span class="string">&quot;python&quot;</span>,</span><br><span class="line">    <span class="string">&quot;--enable-job-insights&quot;</span>          = <span class="literal">false</span>,</span><br><span class="line">    <span class="string">&quot;--enable-observability-metrics&quot;</span> = <span class="literal">false</span>,</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="comment"># ~略~</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>※ちなみに、Glue Python Shell Jobという名前ですが、GlueVersionの設定は動作に影響しないようです。実際に、AWS CLI経由でGlueVersionを1や2に設定しても、Python v3.9の環境で正常に起動していました。</p>
<blockquote>
<p>GlueVersion 設定は Python シェルジョブの動作に影響しないため、GlueVersion をインクリメントするメリットはありません。<br>AWS Glue バージョンサポートポリシー</p>
</blockquote>
<ul>
<li>aws_glue_job</li>
</ul>
<h2 id="さいごに">さいごに</h2><p>Python Shellをはじめて使う時に戸惑いそうな仕様について共有しました。</p>
<p>今回の記事では、ややPython Shellのネガティブな内容が多くなりましたが、実行時間制限がLambdaの15分よりもはるかに長い48時間であり、サーバーレス環境で実行できる等のメリットがあるため、Lambdaでは処理できないケースでは選択肢になると思います。</p>
<p>性能限界が1DPU(4vCPUと16GBのメモリ)ということも頭に入れつつ、もし非機能要件を満たせない可能性がある場合はECSに移行しやすい構成にしておくと安心して利用できると思います。</p>
<p>Python Shellは細かなつまづきポイントが多いので、これから使おうと思っている方のお役に立てれば幸いです🙏</p>
]]></content>
    <summary type="html">業務でGlue Python Shell Job（以降、Python Shell）を利用する機会があったのですが、「Lambdaとかだと簡単にできるのに、Python Shellだとできないんか～」とか、ドキュメント上でPySparkジョブ or Python Shellのどちらについて記載しているのか分かりにくかったりと...</summary>
    <category term="DataEngineering" scheme="https://future-architect.github.io/categories/DataEngineering/"/>
    <category term="AWS" scheme="https://future-architect.github.io/tags/AWS/"/>
    <category term="ETL" scheme="https://future-architect.github.io/tags/ETL/"/>
    <category term="Glue" scheme="https://future-architect.github.io/tags/Glue/"/>
    <category term="Glue Python Shell" scheme="https://future-architect.github.io/tags/Glue-Python-Shell/"/>
    <category term="Python" scheme="https://future-architect.github.io/tags/Python/"/>
    <category term="初心者向け" scheme="https://future-architect.github.io/tags/%E5%88%9D%E5%BF%83%E8%80%85%E5%90%91%E3%81%91/"/>
  </entry>
  <entry>
    <title>PDFをBigqueryにぶっこんで効率よく構造化する</title>
    <link href="https://future-architect.github.io/articles/20250724a/"/>
    <id>https://future-architect.github.io/articles/20250724a/</id>
    <published>2025-07-23T15:00:00.000Z</published>
    <updated>2025-07-23T15:00:00.000Z</updated>
    <author><name>大前七奈</name></author>
    <content type="html"><![CDATA[<img fetchpriority="high" src="/images/2025/20250724a/Gemini_Generated_Image_gwxj4tgwxj4tgwxj_(1).png" alt="" width="1200" height="1200">

<p>AI Tips連載の9日目の記事です。</p>
<p>こんにちは！Energy Transformation Groupの大前七奈です。</p>
<p>最近、Google Agent Development KitやLangChainエコシステムを触る機会が増える中で、多くの企業や研究機関、政府機関が、契約書、報告書、マニュアル、論文、請求書などの重要な情報をPDF形式で保存・配布していることに改めて注目しています。</p>
<p>これらの膨大な情報源から、LLM（大規模言語モデル）が直接情報にアクセスし、理解できるようになることは、実用的なAIソリューションを構築する上で不可欠だと考えています。</p>
<h2 id="PDFとは">PDFとは</h2><p>PDF（Portable Document Format）は、文書をアプリケーションやOS、デバイスに依存せずに表示・印刷できるようにするためのファイル形式です。PDFは「PostScriptの描画モデルに基づき、印刷だけでなく、電子的な文書交換に最適化された静的なファイルフォーマット」として広く利用されています。</p>
<p>PDFの内部構造について、zawakinさんの僕「PDFとは何か知りたい」のQiita記事も参考になります。</p>
<h2 id="課題感">課題感</h2><p>しかし、PDFの内部構造が「グラフィックコマンドの集合体」であるため、単純なコピー＆ペーストではテキスト情報を正確に抽出することが難しいという課題があります。</p>
<p>特に、スキャンされたPDF（画像PDF）の場合はOCR（光学文字認識）が必須となり、この抽出精度がRAG（Retrieval-Augmented Generation）システムの性能に直結します。</p>
<h2 id="PDF抽出フロー">PDF抽出フロー</h2><p>このような課題を解決し、PDFから効率的かつ正確に構造化されたデータを抽出するために、以下のフローを考案しました。このフローでは、特にプロンプト設計とBigQuery MLを活用しています。</p>
<img src="/images/2025/20250724a/sequence.png" alt="" width="800" height="1105" loading="lazy">

<details>
<summary>シーケンス図 (クリックでコードを表示)</summary>

<figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">graph TD</span><br><span class="line">    %% 定義</span><br><span class="line">    subgraph スキーマ抽出&amp;情報抽出</span><br><span class="line">        A[PDFファイル] --&gt; B&#123;GEMINIへ入力&#125;;</span><br><span class="line">        B --&gt; C[GEMINIによるPDF解析];</span><br><span class="line">        C --&gt; D&#123;スキーマ情報の抽出&#125;;</span><br><span class="line">        D -- 抽出されたスキーマをガイドに --&gt; E[GEMINIによる情報抽出];</span><br><span class="line">        %% スキーマをガイドとして再利用</span><br><span class="line">        B -- (元のPDFも引き続き参照) --&gt; E;</span><br><span class="line">        %% GEMINIは元のPDFも再参照できる</span><br><span class="line">        E --&gt; F[抽出されたデータ項目と値];</span><br><span class="line">    end</span><br><span class="line"></span><br><span class="line">    %% 後処理/連携 (自動)</span><br><span class="line">    F --&gt; G[データベース/データ分析ツールへの連携];</span><br></pre></td></tr></table></figure>

</details>

<h2 id="プロンプト設計">プロンプト設計</h2><p>LangChainの公式ドキュメントでは、PDFから構造化データを抽出する際に、Pydanticの型をセットでLLMに渡すことが推奨されています。</p>
<ul>
<li>Build an Extraction Chain | 🦜️🔗 LangChain</li>
</ul>
<p>しかし、社内文書は必ずしもすべて同じスキーマになっているわけではありません。そこで、プロンプト設計の段階で、スキーマ抽出自体もLLMに任せることにしました。これにより、様々な形式のPDFに対応できる柔軟な抽出システムを構築することが可能になります。</p>
<p>以下に示すプロンプトは、PDF解析からJSONデータ抽出までを自動で行うためのものです。</p>
<p>スキーマ抽出と、スキーマに沿った情報抽出の２つタスク分解することで、より汎用性を高めるプロント設計となります。</p>
<figure class="highlight json"><figcaption><span>スキーマ抽出タスク.md</span></figcaption><table><tr><td class="code"><pre><span class="line">あなたは高度なPDF解析とJSONデータ抽出を行うAIです。以下のタスクを順番に実行してください。</span><br><span class="line">**タスク<span class="number">1</span><span class="punctuation">:</span> PDFスキーマの抽出**</span><br><span class="line"></span><br><span class="line">あなたが分析するPDFファイルをアップロードしてください。</span><br><span class="line">PDFファイルを受け取った後、その内容を詳細に分析し、データ構造を正確に記述するJSONスキーマを生成してください。</span><br><span class="line"></span><br><span class="line">スキーマ生成のガイドライン：</span><br><span class="line"></span><br><span class="line">* PDFに記載されている主要な情報カテゴリ（例<span class="punctuation">:</span> <span class="string">&quot;顧客情報&quot;</span><span class="punctuation">,</span> <span class="string">&quot;製品詳細&quot;</span><span class="punctuation">,</span> <span class="string">&quot;合計金額&quot;</span> など）を特定し、それらをJSONオブジェクトのキーとしてください。</span><br><span class="line">* 各キーに対応する値の**データ型**（例<span class="punctuation">:</span> `string`<span class="punctuation">,</span> `number`<span class="punctuation">,</span> `boolean`<span class="punctuation">,</span> `array`<span class="punctuation">,</span> `object`）を明記してください。</span><br><span class="line">* データが**必須**であるか（`<span class="attr">&quot;required&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span>`）、**オプション**であるか（`<span class="attr">&quot;required&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">false</span></span>` または単に`required`キーを省略）を適切に指定してください。</span><br><span class="line">* 繰り返し出現するデータ（例<span class="punctuation">:</span> 複数の製品項目、明細行）がある場合は、それらを**JSONの配列**として表現し、配列内の各要素のスキーマを定義してください。</span><br><span class="line">* 日付や通貨など、特定のフォーマットを持つデータについては、`<span class="string">&quot;format&quot;</span>` プロパティでそのフォーマット（例<span class="punctuation">:</span> `<span class="string">&quot;date&quot;</span>`<span class="punctuation">,</span> `<span class="string">&quot;currency&quot;</span>`）を示すことを検討してください。</span><br><span class="line">* もしデータに特定の制約（最大長、最小値、正規表現パターンなど）がある場合は、それもスキーマに含めてください。</span><br><span class="line"></span><br><span class="line">**スキーマの出力形式<span class="punctuation">:</span>**</span><br><span class="line"></span><br><span class="line">```json</span><br><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;$schema&quot;</span><span class="punctuation">:</span> <span class="string">&quot;[http://json-schema.org/draft-07/schema#](http://json-schema.org/draft-07/schema#)&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;title&quot;</span><span class="punctuation">:</span> <span class="string">&quot;[PDFの内容に応じた適切なタイトル、例: InvoiceSchema]&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;description&quot;</span><span class="punctuation">:</span> <span class="string">&quot;[PDFの内容に関する簡潔な説明、例: スキーマは請求書の一般的な構造を記述します。]&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;type&quot;</span><span class="punctuation">:</span> <span class="string">&quot;object&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;properties&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="comment">// 例:</span></span><br><span class="line">    <span class="comment">// &quot;invoiceNumber&quot;: &#123;</span></span><br><span class="line">    <span class="comment">//   &quot;type&quot;: &quot;string&quot;,</span></span><br><span class="line">    <span class="comment">//   &quot;description&quot;: &quot;請求書番号&quot;,</span></span><br><span class="line">    <span class="comment">//   &quot;pattern&quot;: &quot;^INV-\\d&#123;4&#125;-\\d&#123;3&#125;$&quot;,</span></span><br><span class="line">    <span class="comment">//   &quot;required&quot;: true</span></span><br><span class="line">    <span class="comment">// &#125;,</span></span><br><span class="line">    <span class="comment">// &quot;issueDate&quot;: &#123;</span></span><br><span class="line">    <span class="comment">//   &quot;type&quot;: &quot;string&quot;,</span></span><br><span class="line">    <span class="comment">//   &quot;format&quot;: &quot;date&quot;,</span></span><br><span class="line">    <span class="comment">//   &quot;description&quot;: &quot;発行日&quot;,</span></span><br><span class="line">    <span class="comment">//   &quot;required&quot;: true</span></span><br><span class="line">    <span class="comment">// &#125;,</span></span><br><span class="line">    <span class="comment">// &quot;customerInfo&quot;: &#123;</span></span><br><span class="line">    <span class="comment">//   &quot;type&quot;: &quot;object&quot;,</span></span><br><span class="line">    <span class="comment">//   &quot;properties&quot;: &#123;</span></span><br><span class="line">    <span class="comment">//     &quot;name&quot;: &#123; &quot;type&quot;: &quot;string&quot; &#125;,</span></span><br><span class="line">    <span class="comment">//     &quot;address&quot;: &#123; &quot;type&quot;: &quot;string&quot; &#125;</span></span><br><span class="line">    <span class="comment">//   &#125;,</span></span><br><span class="line">    <span class="comment">//   &quot;required&quot;: [&quot;name&quot;]</span></span><br><span class="line">    <span class="comment">// &#125;,</span></span><br><span class="line">    <span class="comment">// &quot;lineItems&quot;: &#123;</span></span><br><span class="line">    <span class="comment">//   &quot;type&quot;: &quot;array&quot;,</span></span><br><span class="line">    <span class="comment">//   &quot;items&quot;: &#123;</span></span><br><span class="line">    <span class="comment">//     &quot;type&quot;: &quot;object&quot;,</span></span><br><span class="line">    <span class="comment">//     &quot;properties&quot;: &#123;</span></span><br><span class="line">    <span class="comment">//       &quot;description&quot;: &#123; &quot;type&quot;: &quot;string&quot; &#125;,</span></span><br><span class="line">    <span class="comment">//       &quot;quantity&quot;: &#123; &quot;type&quot;: &quot;number&quot; &#125;,</span></span><br><span class="line">    <span class="comment">//       &quot;unitPrice&quot;: &#123; &quot;type&quot;: &quot;number&quot; &#125;</span></span><br><span class="line">    <span class="comment">//     &#125;,</span></span><br><span class="line">    <span class="comment">//     &quot;required&quot;: [&quot;description&quot;, &quot;quantity&quot;, &quot;unitPrice&quot;]</span></span><br><span class="line">    <span class="comment">//   &#125;</span></span><br><span class="line">    <span class="comment">// &#125;,</span></span><br><span class="line">    <span class="comment">// &quot;totalAmount&quot;: &#123;</span></span><br><span class="line">    <span class="comment">//   &quot;type&quot;: &quot;number&quot;,</span></span><br><span class="line">    <span class="comment">//   &quot;description&quot;: &quot;合計金額&quot;,</span></span><br><span class="line">    <span class="comment">//   &quot;required&quot;: true</span></span><br><span class="line">    <span class="comment">// &#125;</span></span><br><span class="line">    <span class="comment">// ... その他、PDFの内容に応じたプロパティを追加</span></span><br><span class="line">  <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;required&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">    <span class="comment">// スキーマのトップレベルで必須となるプロパティをリスト</span></span><br><span class="line">  <span class="punctuation">]</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br><span class="line"></span><br></pre></td></tr></table></figure>

<figure class="highlight json"><figcaption><span>スキーマに沿った情報抽出タスク.md</span></figcaption><table><tr><td class="code"><pre><span class="line">**タスク<span class="number">2</span><span class="punctuation">:</span> タスク<span class="number">1</span>のスキーマでPDFからデータ抽出**</span><br><span class="line"></span><br><span class="line">**タスクの実行手順<span class="punctuation">:</span>**</span><br><span class="line"></span><br><span class="line"><span class="number">1.</span>  **スキーマの理解<span class="punctuation">:</span>** 提供されたJSONスキーマの内容を完全に理解し、どの情報項目をどのような形式で抽出する必要があるかを把握してください。</span><br><span class="line"><span class="number">2.</span>  **PDFからのデータ特定<span class="punctuation">:</span>** アップロードされたPDFファイルを分析し、スキーマで定義された各データ項目に対応する箇所を特定してください。</span><br><span class="line"><span class="number">3.</span>  **データ抽出と整形<span class="punctuation">:</span>**</span><br><span class="line">    * スキーマで指定された**データ型**（文字列、数値、真偽値、配列など）に厳密に従って、PDFから値を抽出してください。</span><br><span class="line">    * **配列**として定義されている項目（例<span class="punctuation">:</span> `lineItems`）については、PDF内に存在するすべての該当するエントリを抽出し、JSON配列の要素として含めてください。</span><br><span class="line">    * スキーマで`<span class="attr">&quot;required&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span>`とマークされているが、PDF内に対応するデータが見つからない場合は、`<span class="literal"><span class="keyword">null</span></span>`値として出力してください。</span><br><span class="line">    * スキーマで`<span class="attr">&quot;required&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">false</span></span>`（または`required`プロパティが指定されていない）とマークされており、かつPDF内に対応するデータが見つからない場合は、そのキーを最終的なJSON出力から省略してください。</span><br><span class="line">    * 日付や通貨など、特定のフォーマットがスキーマで指定されている場合は、そのフォーマットに合わせてデータを整形してください。</span><br><span class="line"></span><br><span class="line">**出力形式<span class="punctuation">:</span>**</span><br><span class="line"></span><br><span class="line">抽出されたすべてのデータは、以下の単一のJSONオブジェクトとして出力してください。このJSONオブジェクトは、提供されたJSONスキーマに完全に準拠している必要があります。</span><br><span class="line"></span><br><span class="line">```json</span><br><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="comment">// ここにPDFから抽出された実際のデータを出力します。</span></span><br><span class="line">  <span class="comment">// 例:</span></span><br><span class="line">  <span class="comment">// &quot;invoiceNumber&quot;: &quot;INV-2025-001&quot;,</span></span><br><span class="line">  <span class="comment">// &quot;issueDate&quot;: &quot;2025-07-04&quot;,</span></span><br><span class="line">  <span class="comment">// &quot;customerInfo&quot;: &#123;</span></span><br><span class="line">  <span class="comment">//   &quot;name&quot;: &quot;株式会社ABC&quot;,</span></span><br><span class="line">  <span class="comment">//   &quot;address&quot;: &quot;東京都千代田区1-2-3&quot;</span></span><br><span class="line">  <span class="comment">// &#125;,</span></span><br><span class="line">  <span class="comment">// &quot;lineItems&quot;: [</span></span><br><span class="line">  <span class="comment">//   &#123;</span></span><br><span class="line">  <span class="comment">//     &quot;description&quot;: &quot;商品A&quot;,</span></span><br><span class="line">  <span class="comment">//     &quot;quantity&quot;: 2,</span></span><br><span class="line">  <span class="comment">//     &quot;unitPrice&quot;: 1500</span></span><br><span class="line">  <span class="comment">//   &#125;,</span></span><br><span class="line">  <span class="comment">//   &#123;</span></span><br><span class="line">  <span class="comment">//     &quot;description&quot;: &quot;サービスB&quot;,</span></span><br><span class="line">  <span class="comment">//     &quot;quantity&quot;: 1,</span></span><br><span class="line">  <span class="comment">//     &quot;unitPrice&quot;: 5000</span></span><br><span class="line">  <span class="comment">//   &#125;</span></span><br><span class="line">  <span class="comment">// ],</span></span><br><span class="line">  <span class="comment">// &quot;totalAmount&quot;: 8000</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure>

<h2 id="今回利用するPDFサンプル">今回利用するPDFサンプル</h2><p>来月開催されるGoogle Next Tokyoの登録済みセッションのPDFです。</p>
<img src="/images/2025/20250724a/image.png" alt="image.png" width="1200" height="548" loading="lazy">

<h2 id="準備完了！さて、BigqueryMLで抽出しよう">準備完了！さて、BigqueryMLで抽出しよう</h2><p>上記のプロンプト設計とPDF抽出フローが整えば、いよいよBigQuery MLを使ってPDFからデータを抽出できます。BigQuery MLは、SQLインターフェースを通じて機械学習モデルを利用できるため、データエンジニアやアナリストが手軽にLLMを活用したデータ抽出パイプラインを構築できます。</p>
<p>以下のBigQuery MLのクエリは、PDFを格納したGoogle Cloud Storageのバケットからデータを読み込み、LLM（Gemini 2.0 Flash Experiment）を使って構造化データを抽出する例です。</p>
<figure class="highlight sql"><figcaption><span>GEMINIを利用するモデル作成.sql</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="keyword">CREATE</span> <span class="keyword">OR</span> REPLACE MODEL `gemini_us.gemini<span class="operator">-</span>flash<span class="operator">-</span>connection`</span><br><span class="line">REMOTE <span class="keyword">WITH</span> CONNECTION `us.gemini<span class="operator">-</span>flash<span class="operator">-</span>connection`</span><br><span class="line">OPTIONS (ENDPOINT <span class="operator">=</span> &quot;gemini-2.0-flash-exp&quot;);</span><br></pre></td></tr></table></figure>

<div class="note-container note-info"><span class="note-icon"></span><div>

<p>現時点、Gemini利用可能なリージョンはUSのみです。それ以外のリージョンを選択してモデルすると、エラーが出ます。</p>
</div></div>

<figure class="highlight sql"><figcaption><span>Cloud StorageのPDFを外部テーブルとして作成.sql</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="keyword">CREATE</span> <span class="keyword">EXTERNAL</span> <span class="keyword">TABLE</span> gemini_us.pdf_table</span><br><span class="line"><span class="keyword">WITH</span> CONNECTION `us.gemini<span class="operator">-</span>flash<span class="operator">-</span>connection`</span><br><span class="line">OPTIONS(</span><br><span class="line">  object_metadata <span class="operator">=</span> <span class="string">&#x27;SIMPLE&#x27;</span>,</span><br><span class="line">  uris <span class="operator">=</span> [&quot;gs://path/to/store/pdf/*&quot;]</span><br><span class="line">  );</span><br></pre></td></tr></table></figure>

<figure class="highlight sql"><figcaption><span>ML.GENERATE_TEXTを利用してPDFをGeminiで解析.sql</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="keyword">SELECT</span> ml_generate_text_llm_result</span><br><span class="line"><span class="keyword">FROM</span> ML.GENERATE_TEXT(</span><br><span class="line">  MODEL `gemini_us.gemini<span class="operator">-</span>flash<span class="operator">-</span>connection`,</span><br><span class="line">  <span class="keyword">TABLE</span> `gemini_us.pdf_table`,</span><br><span class="line">  STRUCT (</span><br><span class="line">  <span class="string">&#x27;&#x27;&#x27;</span></span><br><span class="line"><span class="string">  あなたは高度なPDF解析とJSONデータ抽出を行うAIです。以下のタスクを順番に実行してください。</span></span><br><span class="line"><span class="string">  **タスク1: PDFスキーマの抽出**</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">  ....省略....</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">  **タスク2: タスク1のスキーマでPDFからデータ抽出**</span></span><br><span class="line"><span class="string">  **タスクの実行手順:**</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">  ....省略....</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">  &#x27;&#x27;&#x27;</span> <span class="keyword">AS</span> prompt,</span><br><span class="line">  <span class="literal">TRUE</span> <span class="keyword">as</span> FLATTEN_JSON_OUTPUT,</span><br><span class="line">  <span class="number">8192</span> <span class="keyword">as</span> MAX_OUTPUT_TOKENS</span><br><span class="line">  )</span><br><span class="line">);</span><br></pre></td></tr></table></figure>

<div class="note-container note-info"><span class="note-icon"></span><div>

<p>ML.GENERATE_TEXT関数のMAX_OUTPUT_TOKENSがデフォルトで1024です。ユースケースに応じてMAX_OUTPUT_TOKENSを明示的に定義しました。</p>
<p>こちらでGENERATE_TEXTのsyntaxを確認できます。<br>https://cloud.google.com/bigquery/docs/reference/standard-sql/bigqueryml-syntax-generate-text#syntax_for_standard_tables</p>
</div></div>

<h2 id="PDF抽出結果">PDF抽出結果</h2><p><strong>タスク1: PDFスキーマの抽出</strong></p>
<figure class="highlight json"><table><tr><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;$schema&quot;</span><span class="punctuation">:</span> <span class="string">&quot;http://json-schema.org/draft-07/schema#&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;title&quot;</span><span class="punctuation">:</span> <span class="string">&quot;GoogleCloudNextTokyo2025AgendaSchema&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;description&quot;</span><span class="punctuation">:</span> <span class="string">&quot;スキーマはGoogle Cloud Next Tokyo 2025のアジェンダの構造を記述します。&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;type&quot;</span><span class="punctuation">:</span> <span class="string">&quot;object&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;properties&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;eventTitle&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">      <span class="attr">&quot;type&quot;</span><span class="punctuation">:</span> <span class="string">&quot;string&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;description&quot;</span><span class="punctuation">:</span> <span class="string">&quot;イベントタイトル&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;const&quot;</span><span class="punctuation">:</span> <span class="string">&quot;My Agenda: Google Cloud Next Tokyo 2025&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;required&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span></span><br><span class="line">    <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;registrationStatus&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">      <span class="attr">&quot;type&quot;</span><span class="punctuation">:</span> <span class="string">&quot;string&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;description&quot;</span><span class="punctuation">:</span> <span class="string">&quot;登録ステータス&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;const&quot;</span><span class="punctuation">:</span> <span class="string">&quot;登録済みセッション&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;required&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span></span><br><span class="line">    <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;registrationDescription&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">      <span class="attr">&quot;type&quot;</span><span class="punctuation">:</span> <span class="string">&quot;string&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;description&quot;</span><span class="punctuation">:</span> <span class="string">&quot;登録説明&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;const&quot;</span><span class="punctuation">:</span> <span class="string">&quot;登録済みのセッション一覧はこちらです。&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;required&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span></span><br><span class="line">    <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;sessionNotes&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">      <span class="attr">&quot;type&quot;</span><span class="punctuation">:</span> <span class="string">&quot;array&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;description&quot;</span><span class="punctuation">:</span> <span class="string">&quot;セッションに関する注意事項&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;items&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">        <span class="attr">&quot;type&quot;</span><span class="punctuation">:</span> <span class="string">&quot;string&quot;</span></span><br><span class="line">      <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;required&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span></span><br><span class="line">    <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;sessionListTitle&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">      <span class="attr">&quot;type&quot;</span><span class="punctuation">:</span> <span class="string">&quot;string&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;description&quot;</span><span class="punctuation">:</span> <span class="string">&quot;セッションリストのタイトル&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;const&quot;</span><span class="punctuation">:</span> <span class="string">&quot;すべてのセッションを見る&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;required&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span></span><br><span class="line">    <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;sessions&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">      <span class="attr">&quot;type&quot;</span><span class="punctuation">:</span> <span class="string">&quot;array&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;description&quot;</span><span class="punctuation">:</span> <span class="string">&quot;セッションのリスト&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;items&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">        <span class="attr">&quot;type&quot;</span><span class="punctuation">:</span> <span class="string">&quot;object&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;properties&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">          <span class="attr">&quot;sessionType&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">            <span class="attr">&quot;type&quot;</span><span class="punctuation">:</span> <span class="string">&quot;string&quot;</span><span class="punctuation">,</span></span><br><span class="line">            <span class="attr">&quot;description&quot;</span><span class="punctuation">:</span> <span class="string">&quot;セッションタイプ&quot;</span><span class="punctuation">,</span></span><br><span class="line">            <span class="attr">&quot;enum&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">              <span class="string">&quot;基調講演&quot;</span><span class="punctuation">,</span></span><br><span class="line">              <span class="string">&quot;ブレイクアウト&quot;</span><span class="punctuation">,</span></span><br><span class="line">              <span class="string">&quot;ハンズオン&quot;</span><span class="punctuation">,</span></span><br><span class="line">              <span class="string">&quot;スポンサー&quot;</span><span class="punctuation">,</span></span><br><span class="line">              <span class="string">&quot;Dev Night&quot;</span></span><br><span class="line">            <span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line">            <span class="attr">&quot;required&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span></span><br><span class="line">          <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">          <span class="attr">&quot;sessionTitle&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">            <span class="attr">&quot;type&quot;</span><span class="punctuation">:</span> <span class="string">&quot;string&quot;</span><span class="punctuation">,</span></span><br><span class="line">            <span class="attr">&quot;description&quot;</span><span class="punctuation">:</span> <span class="string">&quot;セッションタイトル&quot;</span><span class="punctuation">,</span></span><br><span class="line">            <span class="attr">&quot;required&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span></span><br><span class="line">          <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">          <span class="attr">&quot;sessionDescription&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">            <span class="attr">&quot;type&quot;</span><span class="punctuation">:</span> <span class="string">&quot;string&quot;</span><span class="punctuation">,</span></span><br><span class="line">            <span class="attr">&quot;description&quot;</span><span class="punctuation">:</span> <span class="string">&quot;セッションの説明&quot;</span><span class="punctuation">,</span></span><br><span class="line">            <span class="attr">&quot;required&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span></span><br><span class="line">          <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">          <span class="attr">&quot;sessionCategory&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">            <span class="attr">&quot;type&quot;</span><span class="punctuation">:</span> <span class="string">&quot;string&quot;</span><span class="punctuation">,</span></span><br><span class="line">            <span class="attr">&quot;description&quot;</span><span class="punctuation">:</span> <span class="string">&quot;セッションカテゴリ&quot;</span><span class="punctuation">,</span></span><br><span class="line">            <span class="attr">&quot;required&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span></span><br><span class="line">          <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">          <span class="attr">&quot;sessionTime&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">            <span class="attr">&quot;type&quot;</span><span class="punctuation">:</span> <span class="string">&quot;string&quot;</span><span class="punctuation">,</span></span><br><span class="line">            <span class="attr">&quot;description&quot;</span><span class="punctuation">:</span> <span class="string">&quot;セッション時間&quot;</span><span class="punctuation">,</span></span><br><span class="line">            <span class="attr">&quot;required&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span></span><br><span class="line">          <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">          <span class="attr">&quot;sessionStatus&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">            <span class="attr">&quot;type&quot;</span><span class="punctuation">:</span> <span class="string">&quot;string&quot;</span><span class="punctuation">,</span></span><br><span class="line">            <span class="attr">&quot;description&quot;</span><span class="punctuation">:</span> <span class="string">&quot;セッションステータス&quot;</span><span class="punctuation">,</span></span><br><span class="line">            <span class="attr">&quot;enum&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">              <span class="string">&quot;登録済みセッションから削除&quot;</span></span><br><span class="line">            <span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line">            <span class="attr">&quot;required&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span></span><br><span class="line">          <span class="punctuation">&#125;</span></span><br><span class="line">        <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;required&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">          <span class="string">&quot;sessionType&quot;</span><span class="punctuation">,</span></span><br><span class="line">          <span class="string">&quot;sessionTitle&quot;</span><span class="punctuation">,</span></span><br><span class="line">          <span class="string">&quot;sessionDescription&quot;</span><span class="punctuation">,</span></span><br><span class="line">          <span class="string">&quot;sessionCategory&quot;</span><span class="punctuation">,</span></span><br><span class="line">          <span class="string">&quot;sessionTime&quot;</span><span class="punctuation">,</span></span><br><span class="line">          <span class="string">&quot;sessionStatus&quot;</span></span><br><span class="line">        <span class="punctuation">]</span></span><br><span class="line">      <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;required&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span></span><br><span class="line">    <span class="punctuation">&#125;</span></span><br><span class="line">  <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;required&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">    <span class="string">&quot;eventTitle&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="string">&quot;registrationStatus&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="string">&quot;registrationDescription&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="string">&quot;sessionNotes&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="string">&quot;sessionListTitle&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="string">&quot;sessions&quot;</span></span><br><span class="line">  <span class="punctuation">]</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure>

<p><strong>タスク2: タスク1のスキーマでPDFからデータ抽出</strong></p>
<figure class="highlight json"><table><tr><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;eventTitle&quot;</span><span class="punctuation">:</span> <span class="string">&quot;My Agenda: Google Cloud Next Tokyo 2025&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;registrationStatus&quot;</span><span class="punctuation">:</span> <span class="string">&quot;登録済みセッション&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;registrationDescription&quot;</span><span class="punctuation">:</span> <span class="string">&quot;登録済みのセッション一覧はこちらです。&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;sessionNotes&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">    <span class="string">&quot;セッションが定員に達した場合、登録受付を終了します。&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="string">&quot;事前のセッション登録は、座席の確保を約束するものではありません。&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="string">&quot;抽選制、または対象者優先のセッションを登録された場合は、参加の可否を後日改めてご案内します。&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="string">&quot;当日の受講票は、7月下旬以降に順次お送りする予定です。&quot;</span></span><br><span class="line">  <span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;sessionListTitle&quot;</span><span class="punctuation">:</span> <span class="string">&quot;すべてのセッションを見る&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;sessions&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">    <span class="punctuation">&#123;</span></span><br><span class="line">      <span class="attr">&quot;sessionType&quot;</span><span class="punctuation">:</span> <span class="string">&quot;基調講演&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;sessionTitle&quot;</span><span class="punctuation">:</span> <span class="string">&quot;Day1 基調講演&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;sessionDescription&quot;</span><span class="punctuation">:</span> <span class="string">&quot;D1-KEYNOTE・AIと機械学習&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;sessionCategory&quot;</span><span class="punctuation">:</span> <span class="string">&quot;D1-KEYNOTE・AIと機械学習&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;sessionTime&quot;</span><span class="punctuation">:</span> <span class="string">&quot;8/5(火)・10:00 - 11:30&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;sessionStatus&quot;</span><span class="punctuation">:</span> <span class="string">&quot;登録済みセッションから削除&quot;</span></span><br><span class="line">    <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="punctuation">&#123;</span></span><br><span class="line">      <span class="attr">&quot;sessionType&quot;</span><span class="punctuation">:</span> <span class="string">&quot;基調講演&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;sessionTitle&quot;</span><span class="punctuation">:</span> <span class="string">&quot;Day2 基調講演&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;sessionDescription&quot;</span><span class="punctuation">:</span> <span class="string">&quot;D2-KEYNOTE・AIと機械学習&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;sessionCategory&quot;</span><span class="punctuation">:</span> <span class="string">&quot;D2-KEYNOTE・AIと機械学習&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;sessionTime&quot;</span><span class="punctuation">:</span> <span class="string">&quot;8/6(水)・10:00 - 11:30&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;sessionStatus&quot;</span><span class="punctuation">:</span> <span class="string">&quot;登録済みセッションから削除&quot;</span></span><br><span class="line">    <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="punctuation">&#123;</span></span><br><span class="line">      <span class="attr">&quot;sessionType&quot;</span><span class="punctuation">:</span> <span class="string">&quot;ブレイクアウト&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;sessionTitle&quot;</span><span class="punctuation">:</span> <span class="string">&quot;Google Kubernetes Engine (GKE)10周年! GKEとCloud Run の最新機能紹介と...&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;sessionDescription&quot;</span><span class="punctuation">:</span> <span class="string">&quot;D1-APP-01・アプリケーション開発&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;sessionCategory&quot;</span><span class="punctuation">:</span> <span class="string">&quot;D1-APP-01・アプリケーション開発&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;sessionTime&quot;</span><span class="punctuation">:</span> <span class="string">&quot;8/5(火)・12:00 - 12:30&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;sessionStatus&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">null</span></span></span><br><span class="line">    <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="punctuation">&#123;</span></span><br><span class="line">      <span class="attr">&quot;sessionType&quot;</span><span class="punctuation">:</span> <span class="string">&quot;ブレイクアウト&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;sessionTitle&quot;</span><span class="punctuation">:</span> <span class="string">&quot;AI ドリブンでのECサイト顧客体験の向上施策 - Vertex Al Search for Commerce とは&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;sessionDescription&quot;</span><span class="punctuation">:</span> <span class="string">&quot;D1-AIML-02・AIと機械学習&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;sessionCategory&quot;</span><span class="punctuation">:</span> <span class="string">&quot;D1-AIML-02・AIと機械学習&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;sessionTime&quot;</span><span class="punctuation">:</span> <span class="string">&quot;8/5(火)・13:00 - 13:30&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;sessionStatus&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">null</span></span></span><br><span class="line">    <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="punctuation">&#123;</span></span><br><span class="line">      <span class="attr">&quot;sessionType&quot;</span><span class="punctuation">:</span> <span class="string">&quot;ハンズオン&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;sessionTitle&quot;</span><span class="punctuation">:</span> <span class="string">&quot;ローコードでAI エージェント開発! Conversational Agents と Application Integration の...&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;sessionDescription&quot;</span><span class="punctuation">:</span> <span class="string">&quot;D1-HO-02 AIと機械学習&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;sessionCategory&quot;</span><span class="punctuation">:</span> <span class="string">&quot;D1-HO-02 AIと機械学習&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;sessionTime&quot;</span><span class="punctuation">:</span> <span class="string">&quot;8/5(火)・14:00 - 15:30&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;sessionStatus&quot;</span><span class="punctuation">:</span> <span class="string">&quot;登録済みセッションから削除&quot;</span></span><br><span class="line">    <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="punctuation">&#123;</span></span><br><span class="line">      <span class="attr">&quot;sessionType&quot;</span><span class="punctuation">:</span> <span class="string">&quot;ブレイクアウト&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;sessionTitle&quot;</span><span class="punctuation">:</span> <span class="string">&quot;Vertex Al で実現: 購買データ ×約1億IDの人流データによる次世代広告ターゲティング&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;sessionDescription&quot;</span><span class="punctuation">:</span> <span class="string">&quot;D1-AIML-06・AIと機械学習&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;sessionCategory&quot;</span><span class="punctuation">:</span> <span class="string">&quot;D1-AIML-06・AIと機械学習&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;sessionTime&quot;</span><span class="punctuation">:</span> <span class="string">&quot;8/5(火)・16:00 - 16:30&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;sessionStatus&quot;</span><span class="punctuation">:</span> <span class="string">&quot;登録済みセッションから削除&quot;</span></span><br><span class="line">    <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="punctuation">&#123;</span></span><br><span class="line">      <span class="attr">&quot;sessionType&quot;</span><span class="punctuation">:</span> <span class="string">&quot;スポンサー&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;sessionTitle&quot;</span><span class="punctuation">:</span> <span class="string">&quot;マーケも CSも、データで動かす LLM × Vertex Al で進化する MOps / CSOps 実践事例&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;sessionDescription&quot;</span><span class="punctuation">:</span> <span class="string">&quot;D1-GL-12・データ分析&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;sessionCategory&quot;</span><span class="punctuation">:</span> <span class="string">&quot;D1-GL-12・データ分析&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;sessionTime&quot;</span><span class="punctuation">:</span> <span class="string">&quot;8/5(火)・17:00 - 17:30&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;sessionStatus&quot;</span><span class="punctuation">:</span> <span class="string">&quot;登録済みセッションから削除&quot;</span></span><br><span class="line">    <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="punctuation">&#123;</span></span><br><span class="line">      <span class="attr">&quot;sessionType&quot;</span><span class="punctuation">:</span> <span class="string">&quot;Dev Night&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;sessionTitle&quot;</span><span class="punctuation">:</span> <span class="string">&quot;Dev Night&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;sessionDescription&quot;</span><span class="punctuation">:</span> <span class="string">&quot;D1-DEVNITコミュニティ&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;sessionCategory&quot;</span><span class="punctuation">:</span> <span class="string">&quot;D1-DEVNITコミュニティ&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;sessionTime&quot;</span><span class="punctuation">:</span> <span class="string">&quot;8/5(火)・18:00 - 20:30&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;sessionStatus&quot;</span><span class="punctuation">:</span> <span class="string">&quot;登録済みセッションから削除&quot;</span></span><br><span class="line">    <span class="punctuation">&#125;</span></span><br><span class="line">  <span class="punctuation">]</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure>

<p>今回の出力結果から、以下の重要な点が確認できました。</p>
<ol>
<li><strong>動的なスキーマ生成の有効性</strong><ul>
<li>タスク1で生成されたJSONスキーマは、PDFの内容を適切に反映しており、各フィールドのデータ型、必須・オプションの指定、さらには列挙型（enum）まで詳細に定義されています。これにより、PDFの内容が多様であっても、LLMが自動的に最適なスキーマを推論できる柔軟性が示されました</li>
</ul>
</li>
<li><strong>正確な情報抽出</strong><ul>
<li>タスク2では、生成されたスキーマに基づいてPDFからデータが正確に抽出されています。特に、複数のセッション情報がsessions配列として正しく構造化されている点、およびセッションステータスが「登録済みセッションから削除」の場合とnullの場合が適切に処理されている点に注目できます。これは、LLMが複雑な構造を持つドキュメントから必要な情報を正確に識別し、指定されたフォーマットで出力する能力が高いことを示しています</li>
</ul>
</li>
<li><strong>BigQuery MLとの連携の容易さ</strong><ul>
<li>BigQuery MLのML.GENERATE_TEXT関数を利用することで、SQLインターフェースから直接LLMを呼び出し、構造化データを取得できることが確認できました。これにより、データパイプラインへの組み込みが非常に容易になり、データ分析環境内でシームレスにPDFからのデータ活用が可能になります</li>
</ul>
</li>
</ol>
<p>これらの結果は、LLMとBigQuery MLを組み合わせることで、これまで手作業や複雑なスクリプトが必要だったPDFからのデータ抽出を、効率的かつ柔軟に自動化できる可能性を強く示しています。</p>
<h2 id="まとめ">まとめ</h2><p>PDFからの効率的な情報抽出を実現するため、LLMとBigQuery MLを組み合わせたソリューションを紹介しました。特に、動的にPDFのスキーマをLLMに抽出させることで、様々な形式の社内文書に対応できる柔軟性の高いシステムを構築できることがご理解いただけたかと思います。</p>
<p>このアプローチにより、PDFに埋もれた貴重な情報をRAGシステムなどで活用できるようになり、より実用的で強力なAIソリューションの構築に繋がるでしょう。</p>
]]></content>
    <summary type="html">Google Agent Development KitやLangChainエコシステムを触る機会が増える中で、多くの企業や研究機関、政府機関が、契約書、報告書、マニュアル、論文、請求書などの重要な情報をPDF形式で保存・配布していることに改めて注目しています。これらの膨大な情報源から、LLM（大規模言語モデル）が直接情報にアクセスし、理解できるようになることは、実用的なAIソリューションを構築する上で不可欠だと考えています。</summary>
    <category term="DataEngineering" scheme="https://future-architect.github.io/categories/DataEngineering/"/>
    <category term="BigQuery" scheme="https://future-architect.github.io/tags/BigQuery/"/>
    <category term="Gemini" scheme="https://future-architect.github.io/tags/Gemini/"/>
    <category term="GoogleCloud" scheme="https://future-architect.github.io/tags/GoogleCloud/"/>
  </entry>
  <entry>
    <title>ローカルKubernetesでdbtをコンテナ化して実行してみる</title>
    <link href="https://future-architect.github.io/articles/20250630a/"/>
    <id>https://future-architect.github.io/articles/20250630a/</id>
    <published>2025-06-29T15:00:00.000Z</published>
    <updated>2025-06-29T15:00:00.000Z</updated>
    <author><name>片岡久人</name></author>
    <content type="html"><![CDATA[<img fetchpriority="high" src="/images/2025/20250630a/top.png" alt="" width="446" height="162">

<h2 id="はじめに">はじめに</h2><p>CNCF連載の6本目です、データ変換ツール「dbt（data build tool）」をDockerコンテナ化し、Kubernetes上で実行する手順を紹介します。</p>
<p>私がCNCFやKubernetesに関して触れたのは今回が初めてですが、Kubernetesの学習の一環として、実際に手を動かしながらクラウドネイティブ技術を体験してみたいという思いで取り組みました。</p>
<h2 id="1-今回の記事でやりたいこと">1. 今回の記事でやりたいこと</h2><p>本記事では、以下のステップを通して、dbtモデルの準備から、それをコンテナにしてKubernetes上で動かすまでを体験します。</p>
<ul>
<li>dbtで作成したデータ変換モデルを、Dockerコンテナ化</li>
<li>作成したコンテナをKubernetesクラスタにデプロイ</li>
<li>Kubernetes上でdbtを実行し、BigQueryにデータを書き込む</li>
</ul>
<h2 id="2-dbtのモデルを実装">2. dbtのモデルを実装</h2><ul>
<li>dbtプロジェクトの作成やモデル実装方法は、dbt Core × BigQueryを使ったデータ変換をやってみたこちらの記事などを参照してください。</li>
<li>本記事では下記のような構成になっていることを想定しています。</li>
</ul>
<figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">&lt;your_repositry&gt;/</span><br><span class="line">├── dbt_trial/</span><br><span class="line">│   ├── models/</span><br><span class="line">│   ├── dbt_project.yml</span><br><span class="line">│   └── profiles.yml</span><br><span class="line">├── kubernetes/</span><br><span class="line">│   └── dbt-run-cron-job.yml</span><br><span class="line">└── Dockerfile</span><br></pre></td></tr></table></figure>

<details><summary>dbt_project.ymlの実装</summary>

<figure class="highlight yaml"><table><tr><td class="code"><pre><span class="line"><span class="attr">name:</span> <span class="string">&#x27;dbt_project&#x27;</span></span><br><span class="line"><span class="attr">version:</span> <span class="string">&#x27;1.0.0&#x27;</span></span><br><span class="line"><span class="attr">profile:</span> <span class="string">&#x27;dbt_project&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="attr">model-paths:</span> [<span class="string">&quot;models&quot;</span>]</span><br><span class="line"><span class="attr">analysis-paths:</span> [<span class="string">&quot;analyses&quot;</span>]</span><br><span class="line"><span class="attr">test-paths:</span> [<span class="string">&quot;tests&quot;</span>]</span><br><span class="line"><span class="attr">seed-paths:</span> [<span class="string">&quot;seeds&quot;</span>]</span><br><span class="line"><span class="attr">macro-paths:</span> [<span class="string">&quot;macros&quot;</span>]</span><br><span class="line"><span class="attr">snapshot-paths:</span> [<span class="string">&quot;snapshots&quot;</span>]</span><br><span class="line"></span><br><span class="line"><span class="attr">clean-targets:</span></span><br><span class="line">  <span class="bullet">-</span> <span class="string">&quot;target&quot;</span></span><br><span class="line">  <span class="bullet">-</span> <span class="string">&quot;dbt_packages&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="attr">models:</span></span><br><span class="line">  <span class="attr">dbt_project:</span></span><br><span class="line">    <span class="string">+persist_docs:</span></span><br><span class="line">      <span class="attr">relation:</span> <span class="literal">true</span></span><br><span class="line">      <span class="attr">columns:</span> <span class="literal">true</span></span><br><span class="line">    <span class="string">+dbt-osmosis:</span> <span class="string">&quot;&#123;model&#125;.yml&quot;</span></span><br><span class="line">    <span class="attr">work_dbt:</span></span><br><span class="line">      <span class="string">+materialized:</span> <span class="string">view</span></span><br><span class="line">      <span class="string">+schema:</span> <span class="string">work_dbt</span></span><br></pre></td></tr></table></figure>

</details>

<details><summary>profiles.ymlの実装</summary>

<figure class="highlight yaml"><table><tr><td class="code"><pre><span class="line"><span class="attr">dbt_trial:</span></span><br><span class="line">  <span class="attr">outputs:</span></span><br><span class="line">    <span class="attr">dev:</span></span><br><span class="line">      <span class="attr">dataset:</span> <span class="string">dbt</span></span><br><span class="line">      <span class="attr">job_execution_timeout_seconds:</span> <span class="number">300</span></span><br><span class="line">      <span class="attr">job_retries:</span> <span class="number">1</span></span><br><span class="line">      <span class="attr">location:</span> <span class="string">asia-northeast1</span></span><br><span class="line">      <span class="attr">method:</span> <span class="string">oauth</span></span><br><span class="line">      <span class="attr">priority:</span> <span class="string">interactive</span></span><br><span class="line">      <span class="attr">project:</span> <span class="string">&lt;your-project&gt;</span> <span class="comment"># ★ここを自身のGCPプロジェクトに修正してください★</span></span><br><span class="line">      <span class="attr">threads:</span> <span class="number">1</span></span><br><span class="line">      <span class="attr">type:</span> <span class="string">bigquery</span></span><br><span class="line">  <span class="attr">target:</span> <span class="string">dev</span></span><br></pre></td></tr></table></figure>

</details>

<h2 id="3-dbtの実装をコンテナ化">3. dbtの実装をコンテナ化</h2><p>ここでは、dbtプロジェクトをDockerコンテナとしてパッケージ化する手順を説明します。前提として、Docker Desktopがインストールされ、稼働していることをご確認ください。Docker DesktopでのKubernetesの利用方法については、公式ドキュメントなどを参考にしてください。</p>
<h3 id="3-1-Dockerイメージを作成">3.1 Dockerイメージを作成</h3><p>dbtプロジェクトを含んだDockerイメージを作成します。dbt公式イメージ（ghcr.io&#x2F;dbt-labs&#x2F;dbt-bigquery）をベースにすることで、簡単に環境を構築できます。</p>
<h4 id="Dockerfileの例">Dockerfileの例</h4><figure class="highlight dockerfile"><table><tr><td class="code"><pre><span class="line"><span class="comment"># dbt公式イメージを利用（BigQuery用）</span></span><br><span class="line"><span class="comment"># URL:https://github.com/dbt-labs/dbt-bigquery/pkgs/container/dbt-bigquery</span></span><br><span class="line"><span class="keyword">FROM</span> --platform=linux/amd64 ghcr.io/dbt-labs/dbt-bigquery:<span class="number">1.9</span>.latest</span><br><span class="line"></span><br><span class="line"><span class="keyword">WORKDIR</span><span class="language-bash"> /usr/app</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># dbtプロジェクトをコンテナ内にコピー</span></span><br><span class="line"><span class="keyword">COPY</span><span class="language-bash"> dbt_trial/ /usr/app/</span></span><br></pre></td></tr></table></figure>

<h3 id="3-2-コンテナのビルドコマンド">3.2 コンテナのビルドコマンド</h3><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">docker build -t dbt_trial .</span><br></pre></td></tr></table></figure>

<ul>
<li>上記コマンドで<code>dbt_trial</code>という名前のDockerイメージが作成されます。</li>
</ul>
<h2 id="4-コンテナ化したものをKubernetesでデプロイする">4. コンテナ化したものをKubernetesでデプロイする</h2><p>作成したdbtコンテナイメージをKubernetesクラスタにデプロイし、実行する準備をします。</p>
<h3 id="4-1-CinfigMap作成">4.1 CinfigMap作成</h3><p>dbtの認証情報が含まれる profiles.yml をKubernetesのConfigMapとして登録します。これにより、機密情報をコンテナイメージに含めることなく、Kubernetesから安全にアプリケーションに提供できます。</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="comment"># dbt_trial配下のprofiles.ymlをConfigMapとして追加</span></span><br><span class="line">kubectl create configmap dbt-profiles --from-file=dbt_trial/profiles.yml</span><br><span class="line"></span><br><span class="line"><span class="comment"># 設定されたConfigMapの一覧を確認</span></span><br><span class="line">kubectl get configmap</span><br><span class="line"></span><br><span class="line"><span class="comment"># ConfigMapの中身を確認できればOK</span></span><br><span class="line">kubectl describe configmap dbt-provile</span><br></pre></td></tr></table></figure>

<h3 id="4-2-Kubernetesの設定ファイル（CronJob）を作成">4.2 Kubernetesの設定ファイル（CronJob）を作成</h3><p>dbtをKubernetes上で実行するための「Jobリソース」の設定ファイルを作成します。Jobリソースは、決められた処理を一度だけ、または定期的に実行するためのものです。今回は、定期実行ではなく手動で実行することを想定しているため、suspend: true（一時停止状態）に設定した「CronJobリソース」をテンプレート（ひな形）として利用します。</p>
<p>ファイル名は kubernetes&#x2F;dbt-run-cron-job.yml とします。</p>
<h4 id="dbt-run-cron-job-yamlの例">dbt-run-cron-job.yamlの例</h4><figure class="highlight yaml"><table><tr><td class="code"><pre><span class="line"><span class="attr">apiVersion:</span> <span class="string">batch/v1</span></span><br><span class="line"><span class="attr">kind:</span> <span class="string">CronJob</span></span><br><span class="line"><span class="attr">metadata:</span></span><br><span class="line">  <span class="attr">name:</span> <span class="string">dbt-run-cron-job</span> <span class="comment"># CronJobリソースの名前</span></span><br><span class="line"><span class="attr">spec:</span></span><br><span class="line">  <span class="attr">schedule:</span> <span class="string">&quot;0 1 * * *&quot;</span></span><br><span class="line">  <span class="attr">suspend:</span> <span class="literal">true</span>  <span class="comment"># 定期実行は不要であるためtrueにしています。</span></span><br><span class="line">  <span class="attr">jobTemplate:</span></span><br><span class="line">    <span class="attr">spec:</span></span><br><span class="line">      <span class="attr">template:</span></span><br><span class="line">        <span class="attr">spec:</span></span><br><span class="line">          <span class="attr">containers:</span></span><br><span class="line">          <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">dbt-runner</span></span><br><span class="line">            <span class="attr">image:</span> <span class="string">my-dbt-bigquery:latest</span></span><br><span class="line">            <span class="attr">imagePullPolicy:</span> <span class="string">Never</span> <span class="comment"># ローカルイメージを使用するため、Neverに設定</span></span><br><span class="line">            <span class="attr">command:</span> [<span class="string">&quot;dbt&quot;</span>, <span class="string">&quot;run&quot;</span>, <span class="string">&quot;--profiles-dir&quot;</span>, <span class="string">&quot;/dbt&quot;</span>]</span><br><span class="line">            <span class="attr">volumeMounts:</span></span><br><span class="line">            <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">dbt-profiles</span></span><br><span class="line">              <span class="attr">mountPath:</span> <span class="string">/dbt/profiles.yml</span></span><br><span class="line">              <span class="attr">subPath:</span> <span class="string">profiles.yml</span></span><br><span class="line">            <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">gcloud-auth</span></span><br><span class="line">              <span class="attr">mountPath:</span> <span class="string">/root/.config/gcloud</span></span><br><span class="line">          <span class="attr">volumes:</span></span><br><span class="line">          <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">dbt-profiles</span></span><br><span class="line">            <span class="attr">configMap:</span></span><br><span class="line">              <span class="attr">name:</span> <span class="string">dbt-profiles</span></span><br><span class="line">          <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">gcloud-auth</span></span><br><span class="line">            <span class="attr">hostPath:</span></span><br><span class="line">              <span class="attr">path:</span> <span class="string">/Users/&lt;your_user_name&gt;/.config/gcloud</span> <span class="comment"># ★ここを自身のPCのパスに修正してください★</span></span><br><span class="line">          <span class="attr">restartPolicy:</span> <span class="string">Never</span></span><br></pre></td></tr></table></figure>

<div class="note-container note-info"><span class="note-icon"></span><div>

<p><strong>BigQueryの認証方式について</strong></p>
<p>上記の例では、ローカル開発環境での簡易性を考慮し、ホストPCの gcloud コマンドで設定された認証情報（OAuth）を hostPath ボリュームとしてコンテナにマウントしています。実運用環境のKubernetesクラスタでBigQueryと連携する場合は、GCPサービスアカウントキーをSecretとして安全にマウントしたり、Workload IdentityのようなKubernetesネイティブな認証方式を利用することが推奨されます。</p>
</div></div>

<h3 id="4-3-Kubernetesにデプロイするコマンド">4.3 Kubernetesにデプロイするコマンド</h3><p>作成したCronJob設定ファイルをKubernetesクラスタに適用します。</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">kubectl apply -f kubernetes/dbt-run-cron-job.yml</span><br><span class="line"></span><br><span class="line"><span class="comment"># 実行結果</span></span><br><span class="line">cronjob.batch/dbt-run-cron-job created</span><br></pre></td></tr></table></figure>

<p>これでKubernetesクラスタにCronJobリソースが登録されました。このCronJobは、dbtコマンドを実行するためのJobのテンプレートとして機能します。</p>
<h3 id="4-4-デプロイが成功したか確認するコマンド">4.4 デプロイが成功したか確認するコマンド</h3><p>CronJobリソースが正しくデプロイされたかを確認します。</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">kubectl get cronjobs</span><br><span class="line"></span><br><span class="line"><span class="comment"># 実行結果</span></span><br><span class="line">NAME               SCHEDULE    TIMEZONE   SUSPEND   ACTIVE   LAST SCHEDULE   AGE</span><br><span class="line">dbt-run-cron-job   0 1 * * *   &lt;none&gt;     True      0        &lt;none&gt;          17s</span><br></pre></td></tr></table></figure>

<h2 id="5-Kubernetes上でdbtを実行する">5. Kubernetes上でdbtを実行する</h2><p>デプロイされたCronJobをテンプレートとして利用し、dbtを実行するJobを作成します。</p>
<h3 id="5-1-Jobを実行するコマンド">5.1 Jobを実行するコマンド</h3><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">kubectl create job --from=cronjob/dbt-job dbt-job-run</span><br></pre></td></tr></table></figure>

<h3 id="5-2-実行が成功したか確認">5.2 実行が成功したか確認</h3><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">kubectl get <span class="built_in">jobs</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 実行結果</span></span><br><span class="line">NAME      STATUS     COMPLETIONS   DURATION   AGE</span><br><span class="line">dbt-run   Complete   1/1           14s        16s</span><br></pre></td></tr></table></figure>

<ul>
<li>Jobの COMPLETIONS が 1&#x2F;1 や STATUS が Complete になっていれば、OKです。</li>
</ul>
<h3 id="5-3-実行結果の確認">5.3 実行結果の確認</h3><figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">kubectl logs job/dbt-run <span class="comment"># applyで作成したJob名（例：dbt-run）に合わせてコマンドを修正してください。</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 実行結果</span></span><br><span class="line">09:56:38  Running with dbt=1.9.0</span><br><span class="line">09:56:38  Registered adapter: bigquery=1.9.0</span><br><span class="line">09:56:38  Unable to <span class="keyword">do</span> partial parsing because of a version mismatch</span><br><span class="line">09:56:39  Found 4 models, 4 seeds, 13 data tests, 1 <span class="built_in">source</span>, 487 macros</span><br><span class="line">09:56:39</span><br><span class="line">09:56:39  Concurrency: 1 threads (target=<span class="string">&#x27;dev&#x27;</span>)</span><br><span class="line">09:56:39</span><br><span class="line">WARNING:google.auth._default:No project ID could be determined. Consider running `gcloud config <span class="built_in">set</span> project` or setting the GOOGLE_CLOUD_PROJECT environment variable</span><br><span class="line">09:56:42  1 of 4 START sql table model dbt.my_first_dbt_model ............................ [RUN]</span><br><span class="line">09:56:44  1 of 4 OK created sql table model dbt.my_first_dbt_model ....................... [CREATE TABLE (2.0 rows, 0 processed) <span class="keyword">in</span> 2.73s]</span><br><span class="line">09:56:44  2 of 4 START sql view model dbt_staging.stg_sales .............................. [RUN]</span><br><span class="line">09:56:45  2 of 4 OK created sql view model dbt_staging.stg_sales ......................... [CREATE VIEW (0 processed) <span class="keyword">in</span> 0.79s]</span><br><span class="line">09:56:45  3 of 4 START sql view model dbt.my_second_dbt_model ............................ [RUN]</span><br><span class="line">09:56:46  3 of 4 OK created sql view model dbt.my_second_dbt_model ....................... [CREATE VIEW (0 processed) <span class="keyword">in</span> 0.71s]</span><br><span class="line">09:56:46  4 of 4 START sql view model dbt_mart.mart_sales_amount_per_day ................. [RUN]</span><br><span class="line">09:56:47  4 of 4 OK created sql view model dbt_mart.mart_sales_amount_per_day ............ [CREATE VIEW (0 processed) <span class="keyword">in</span> 0.84s]</span><br><span class="line">09:56:47</span><br><span class="line">09:56:47  Finished running 1 table model, 3 view models <span class="keyword">in</span> 0 hours 0 minutes and 7.18 seconds (7.18s).</span><br><span class="line">09:56:47</span><br><span class="line">09:56:47  Completed successfully</span><br><span class="line">09:56:47</span><br><span class="line">09:56:47  Done. PASS=4 WARN=0 ERROR=0 SKIP=0 TOTAL=4</span><br></pre></td></tr></table></figure>

<p>ログからdbtの実行が成功していることが確認できます。最後に、BigQueryのコンソールで実際にテーブルが作成されているかを確認しましょう。</p>
<h2 id="まとめ">まとめ</h2><p>dbtプロジェクトをDockerコンテナ化し、KubernetesのCronJobテンプレートを利用してJobとして実行する一連の手順をご紹介しました。</p>
<p>ローカル環境でも本番環境さながらのデータパイプライン構築が体験でき、ConfigMapによる設定ファイルの管理、Kubernetes Jobリソースの利用、ホストパスボリュームを用いた認証情報のマウントなど、Kubernetesの基本的な概念と実践的な使い方を学ぶことができました。</p>
<p>これを応用することで、CI&#x2F;CDパイプラインへの組み込みや、クラウド環境のKubernetesクラスタへのデプロイなどの足掛かりができたので、挑戦していこうと思います。</p>
]]></content>
    <summary type="html">データ変換ツール「dbt（data build tool）」をDockerコンテナ化し、Kubernetes上で実行する手順を紹介します。</summary>
    <category term="DataEngineering" scheme="https://future-architect.github.io/categories/DataEngineering/"/>
    <category term="Docker" scheme="https://future-architect.github.io/tags/Docker/"/>
    <category term="Kubernetes" scheme="https://future-architect.github.io/tags/Kubernetes/"/>
    <category term="dbt" scheme="https://future-architect.github.io/tags/dbt/"/>
    <category term="コンテナ" scheme="https://future-architect.github.io/tags/%E3%82%B3%E3%83%B3%E3%83%86%E3%83%8A/"/>
  </entry>
  <entry>
    <title>組織のデータを＜コモンズ＞として管理してみてはどうだろうか？</title>
    <link href="https://future-architect.github.io/articles/20250528a/"/>
    <id>https://future-architect.github.io/articles/20250528a/</id>
    <published>2025-05-27T15:00:00.000Z</published>
    <updated>2025-05-27T15:00:00.000Z</updated>
    <author><name>赤倉優蔵</name></author>
    <content type="html"><![CDATA[
<img fetchpriority="high" src="/images/2025/20250528a/normal_1b97659a-1c2f-425e-86b7-0b11e87619a4.jpg" alt="" width="200" height="284">


<p>春の入門祭り2025の25本目の記事です。</p>
<h2 id="はじめに">はじめに</h2><p>はじめまして、TIG（Techonology Innovation Group）の赤倉です。</p>
<p>インターネットの黎明期に報道機関のシステムエンジニアとしてキャリアをスタートした就職氷河期世代で、フューチャーには2022年4月にキャリア入社、ネットワーク技術の進化・社会浸透を横目にキャリアを歩んできました。</p>
<p>そのような私が近年興味を抱き独学を進めている分野がデジタルアーカイブ <sup id="fnref:1">1</sup> やレコードキーピング <sup id="fnref:2">2</sup> で、これらの知見をデータマネジメントの分野に活かすことはできないか模索しはじめました。</p>
<ul>
<li>データマネジメントにおいてガバナンスを利かせることは必要不可欠である一方、雇用の流動性が高まり、IT環境の変化も著しいなかで、トップダウンによる命令系統によってこれを維持し続けることは、難しいのではないか？</li>
<li>データを利用する一人一人が自ら考え、自律的・倫理的な行動を促すようなガバナンスの形を模索してみてはどうだろうか？</li>
</ul>
<p>こうした一連の思考から生まれた問が本稿のタイトルになっています。</p>
<p>本稿は特定の技術やスキルに言及するものではなく、思考実験の域にある論考です。タイトルに興味を覚えられたら読み進めてみてください。</p>
<h2 id="知識コモンズ研究とデータマネジメント">知識コモンズ研究とデータマネジメント</h2><p>少し前、組織の知識を組織の共有材＜コモンズ＞として取り扱うための研究分野「知識コモンズ」があることを知りました。知識コモンズ研究の射程や展開、現在の状況については西川開氏の著書で紹介されています。</p>
<ul>
<li>知識コモンズとは何か - 株式会社　勁草書房</li>
</ul>
<p>知識コモンズの定義は時代とともに変遷してきているため、厳密には定まっていませんが、知識資源を「科学や芸術、社会活動の結果として生み出される多種多様な知識や情報、データ（同書P2）」とし、近年の知識コモンズ研究では、この知識資源そのもの、およびこれを生成・共有する際の管理制度や管理制度を運営する組織・コミュニティのガバナンスを研究対象として捉えています。</p>
<p>知識コモンズ研究の成果はクリエイティブ・コモンズ・ライセンスに結実し、また、「オープンサイエンスやデジタルアーカイブに関連するプロジェクトの制度設計を支える理論的根拠として活用されるとともに、現在ではEUのデータ政策にも影響をおよぼしつつある（同書P4）」など、現在進行形で現実世界に影響を及ぼし続けています。</p>
<p>一方、データを企業にとっての重要な＜資産＞とみなし、これを効率的・効果的に管理するための活動に「データマネジメント」があります。データマネジメントの実践者向けに書かれた書籍『データマネジメント知識体系ガイド』は耳にされた方、あるいは利用されている方も多いのではないでしょうか。</p>
<ul>
<li>データマネジメント知識体系ガイド 第二版 改定新版 | 日経BOOKプラス</li>
</ul>
<p>本書は組織がデータを管理（データマネジメント）するための智恵が包括的・体系的にまとめられたガイドラインで、2011年に1st editionが出版されて以降、2017年に2nd edition、2024年に2nd editionの改訂新版が刊行されるのと並行し、日本語を含む多くの言語にも翻訳され、世界中の多くの組織で利用されています。</p>
<p>知識コモンズ研究の射程は公共コミュニティ、データマネジメントの射程は企業コミュニティと、適用範囲は異なりますが、いずれも組織的な知識資源の取り扱い方、ガバナンスのあり方を探求する、という面で共通性があります。そうであるならば、知識コモンズ研究の成果、例えばガバナンスの評価方法や体制・制度の作り方について、データマネジメントの領域に活かすことはできないだろうかと考えはじめ、その1つの切り口が本稿のタイトルにある問です。</p>
<p>問に向き合う事前準備として、以下、両分野で入口となりそうな私見を展開してみます。</p>
<h2 id="データマネジメント">データマネジメント</h2><p>データマネジメントについては、冒頭で触れた『データマネジメント知識体系ガイド（以下DMBOK）』は抑えておきたいところですが、ボリュームがあるため通読には向かず、また、耳慣れない定義用語が多く難読感は否めません。興味・感心がある領域から読み進めるのも手ですが、それでも最初に何等かのきっかけが欲しいところです。</p>
<p>そこでまず、同書の監訳に携わられた Metafindコンサルティングによるブログ記事 でDMBOKの概観を掴み、そのうえで、以下のような順序で読み進めてみることをお勧めします。私の探求は途に就いたばかりですが、この読み方をとることで、何とか読み進めることができています。</p>
<ol>
<li>第1章_データマネジメント &#x2F; 第1節_イントロダクション（※データマネジメントの位置付けを理解する）</li>
<li>第2章_データ取扱倫理（※データマネジメントの意義を捉える）</li>
<li>第1章_データマネジメント &#x2F; 第2節以降（※DMBOKの背景知識を理解する）</li>
<li>第3章_データガバナンス（※DMBOK第3章以降の記載パターンを把握する）</li>
<li>興味・関心のある領域の章</li>
</ol>
<p>第2章の優先度を上げたのは、倫理を扱うこの章が「なぜデータマネジメントが必要なのか」を考えるきっかけとして優れていると感じたためです。EUのGDPRやアメリカのADPPAをはじめ、世界各国でデータ取扱に関わるコンプライアンスの遵守が重視されていく中で、データマネジメントにおいて、行動の良し悪しを自ら判断するための基準になる倫理観を育むことはとても重要なことだと考えています。倫理観がなければ、データの誤用・悪用はなくなりません。</p>
<p>また、フューチャーの有志による『データマネジメント設計ガイドライン』も理解を深める材料にしてみてください。このガイドラインの作成には私もDMBOKを片手に参加させていただきました。</p>
<ul>
<li>データマネジメント設計ガイドライン | Future Enterprise Arch Guidelines</li>
</ul>
<p>最近の動向を抑えるにあたっては、採用するテクノロジーの選別、アーキテクチャ設計時に考慮すべきポイントなどの観点から、オライリーの『大規模データ管理』も参考になります。</p>
<ul>
<li>大規模データ管理 第2版 - O’Reilly Japan</li>
</ul>
<h2 id="知識コモンズ研究">知識コモンズ研究</h2><p>一方の知識コモンズ研究については、『知識コモンズとは何か』を通読することで研究領域、研究手法、研究成果を読み解くことができますが、概観すると次のようになります。</p>
<ul>
<li>知識コモンズ研究がはじまった1990年代当初は知的財産権法のあり方に研究の焦点が集まっていた</li>
<li>その後その焦点は、知識資源そのもの、知識資源の生産と共有をおこなう個々の事例におけるガバナンスのありように拡張してきた</li>
<li>研究の過程において知識資源とその管理制度の類型化が進められ、これらを分析するアプローチやフレームワーク、評価基準が開発されてきた</li>
</ul>
<p>また、同書のなかで著者の西川開氏は知識コモンズ研究の意義を3点、挙げられていますが、そのなかで私が注目しているのは次の点です。</p>
<blockquote>
<p>知識資源のガバナンスという複雑かつ曖昧な事象を見通すためのツールとして有用で（中略）こうした知見は、ガバナンスの制度設計をおこなおうとする際には議論の基盤となり、既存の事例におけるガバナンスの方法を分析する際にはその解像度を高めてくれる一種のレンズとして機能する（P170）</p>
</blockquote>
<p>この意義を引き受けたうえで、データマネジメントに知識コモンズ研究の成果を取り入れるとは具体的にどのようなことか、私の展望を2点、記述してみます。</p>
<p>1点目は「データマネジメントの相対化」です。</p>
<p>自組織におけるデータマネジメントの制度設計・ガバナンス状況を知識コモンズの分析に耐えうるような形でモデル化できるようになれば、他組織・他企業のそれと比較できるようになると考えています。</p>
<p>客観的に比較できる状態をつくることができれば、他組織と比較して、自組織は何がよくて何が悪いのか、何をすればよりよい状態にできるのか、客観的な視点を手に入れることに繋がります。</p>
<p>2点目は「データマネジメント自律化への道筋を見出す」です。</p>
<p>冒頭で少し触れましたが、データマネジメントの目的の1つとしてよく耳にするのが「データの民主化」ですが、私はこれは「データマネジメントの自律化」を目指すものだと考えています。</p>
<blockquote>
<ul>
<li>データを利用する一人一人が自ら考え、自律的・倫理的な行動を促すようなガバナンスの形を模索してみてはどうだろうか？</li>
</ul>
</blockquote>
<p>そしてこの自律化の理論的支柱を知識コモンズ研究の成果に見出すことができるのではないか、との展望があります。</p>
<h2 id="おわりに">おわりに</h2><p>普段の業務でデータマネジメントに携わる傍ら、データマネジメントに対して思うことがありました。例えば…</p>
<ul>
<li>データマネジメントはとっつきにくい、少し肩の力を落として楽しみながら取り組めないか</li>
<li>データマネジメントは権威主義の香りがする、データを民主化するのであればその手段も民主化する必要があるのではないか</li>
<li>日本の組織に適したデータ管理の形があるのではないか</li>
</ul>
<p>そのような中で出会ったのが知識コモンズ研究です。知識コモンズの研究成果であるデータガバナンスの現状を分析する手法、あるいは、制度設計のあり方は、データマネジメントに活かせるのではないか、そう考えています。</p>
<p>少し脱線すると、冒頭で触れたデジタルアーカイブやレコードキーピングの知見もデータマネジメントに取り込む余地がある、そうすることでより良いデータマネジメントの形を追及していくことができる、とも考えています。</p>
<p>もし本稿がデータマネジメントそして知識コモンズに対し興味を抱くきっかけとなった方がいらっしゃれば、どこかで会話しましょう。ぜひお声がけください。</p>
<p>私の試みはまだ思考実験の段階にあり、これから少しずつ職務を通じ実践に繋げていきながら、じっくり練り上げていってみたいと思います。</p>
<h2 id="補記：＜コモンズ＞について">補記：＜コモンズ＞について</h2><p>そもそもコモンズとは何なのか。本稿では＜コモンズ＞としていましたが、その上位概念にあたる＜コモン＞とあわせ、理解を深めるために役に立ちそうなドキュメントをいくつか挙げておきます。</p>
<ul>
<li>コモンズが開く地域の未来 ｜日本総研</li>
<li>コモンズ（コモン） | 連載コラム | 情報・知識＆オピニオン imidas - イミダス</li>
</ul>
<p>書籍では斎藤幸平氏、松本卓也氏らによる著書がお勧めです。本書を読み進めることで、「コモンとは何か」「何故いまコモンなのか」が見えてきます。</p>
<ul>
<li>コモンの「自治」論／斎藤 幸平／松本 卓也／白井 聡／松村 圭一郎／岸本 聡子／木村 あや／藤原 辰史 | 集英社　―　SHUEISHA　―</li>
</ul>
<p>知識をコモンズとして運用する営みは図書館でも実践されていますが、なかでも私が注目しているのは、「まちライブラリー」の実践です。</p>
<ul>
<li>「まちライブラリー」の研究 | 「個」が主役になれる社会的資本づくり | みすず書房</li>
</ul>
<p>まだ黎明期にありますが、「文化的コモンズ」の実践や知見にもデータマネジメントに活かせることがあるように感じています。</p>
<ul>
<li>『ライブラリー・リソース・ガイド』（LRG）第51号（特集「文化的コモンズ 地域で展開する」）を刊行 - アカデミック・リソース・ガイド（arg）</li>
</ul>
<p>これらの思想や実践に触れることで、広い視野を持ちながら知識コモンズの理解を深めていくことができるのでは、と考えています。</p>
<div id="footnotes"><hr><div id="footnotelist"><ol style="list-style:none; padding-left: 0;"><li id="fn:1"><span style="vertical-align: top; padding-right: 10px;">1.</span><span style="vertical-align: top;">有形・無形の文化財をデジタル情報として記録し、劣化なく永久保存するとともに、ネットワークなどを用いて提供すること。※『図書館情報学用語辞典 第5版（2020年、丸善出版）』より抜粋</span> ↩</li><li id="fn:2"><span style="vertical-align: top; padding-right: 10px;">2.</span><span style="vertical-align: top;">業務行為の完全かつ正確で信頼できる証拠としての記録を作成し、保持することにかかわるあらゆる営為を指す概念。※『アーカイブズ学用語辞典（2024年、柏書房）』より抜粋</span> ↩</li></ol></div></div>]]></content>
    <summary type="html">私が近年興味を抱き独学を進めている分野がデジタルアーカイブやレコードキーピングで、これらの知見をデータマネジメントの分野に活かすことはできないか模索しはじめました。</summary>
    <category term="DataEngineering" scheme="https://future-architect.github.io/categories/DataEngineering/"/>
    <category term="データマネジメント" scheme="https://future-architect.github.io/tags/%E3%83%87%E3%83%BC%E3%82%BF%E3%83%9E%E3%83%8D%E3%82%B8%E3%83%A1%E3%83%B3%E3%83%88/"/>
    <category term="ナレッジ管理" scheme="https://future-architect.github.io/tags/%E3%83%8A%E3%83%AC%E3%83%83%E3%82%B8%E7%AE%A1%E7%90%86/"/>
  </entry>
  <entry>
    <title>初めてのAmazon SageMaker Unified Studio</title>
    <link href="https://future-architect.github.io/articles/20250527a/"/>
    <id>https://future-architect.github.io/articles/20250527a/</id>
    <published>2025-05-26T15:00:00.000Z</published>
    <updated>2025-05-26T15:00:00.000Z</updated>
    <author><name>中神孝士</name></author>
    <content type="html"><![CDATA[<p>春の入門祭り2025の24本目の記事です。</p>
<h2 id="はじめに">はじめに</h2><p>こんにちは、TIG中神です。</p>
<p>カタログ管理やデータ活用ツールとしてAmazon SageMaker Unified Studioを調査・検証したので触ってみた所感やポイントなどつらつらとまとめます。</p>
<h2 id="Amazon-SageMaker-Unified-Studioとは？">Amazon SageMaker Unified Studioとは？</h2><p>みなさんAWSでSageMakerと聞くと何を想像するでしょうか？ おそらくAWSのSageMakerと聞くと機械学習のサービスを思い浮かべる方も多いかと思います（私も同じ認識でした）</p>
<p>実はAWSのデータマネジメント関連のサービスがSagemakerシリーズとして統合されることがre:Invent2024で発表され、その中で中核を担うAmazon SageMaker Unified Studioが2025年3月13日に一般提供されました。</p>
<p>公式のページのSageMakerの配下にはおなじみのRedshiftやGlueやAthenaなども記載されており、これらのデータを扱う既存のサービス群との連携を強化し、より利便性を高めていくのがAmazon SageMaker Unified Studioの役割になっていくと思います。</p>
<img fetchpriority="high" src="/images/2025/20250527a/{C20B3C58-89A7-4E12-AEBE-2D121A57B936}.png" alt="{C20B3C58-89A7-4E12-AEBE-2D121A57B936}.png" width="1200" height="783">

<p>引用：Amazon SageMaker（機械学習モデルを大規模に構築、トレーニング、デプロイ）| AWS</p>
<p>SageMaker Unified Studioでは以下の図のようにデータを活用していくための様々な機能が包含されています。</p>
<img src="/images/2025/20250527a/{753A5134-D8F7-460C-9AA2-3120D57E90C8}.png" alt="{753A5134-D8F7-460C-9AA2-3120D57E90C8}.png" width="846" height="876" loading="lazy">

<p>引用：Amazon SageMaker（機械学習モデルを大規模に構築、トレーニング、デプロイ）| AWS</p>
<ul>
<li><strong>Unified Studio</strong>： データ分析とAIのツールを単一の開発環境として提供する機能</li>
<li><strong>Lakehouse</strong>： SageMaker Lakehouseを利用したS3やRedshift、および外部DWHと連携したデータアクセス機能</li>
<li><strong>カタログ</strong>： Amazon Datazoneを利用したデータのカタログ機能</li>
<li><strong>SQL分析</strong>： Amazon Redshiftを利用したSQL分析機能</li>
<li><strong>データ処理</strong>： Amazon AthenaやGlueを利用したデータ処理機能</li>
<li><strong>モデル開発</strong>： Amazon Sagemaker AIを利用したモデル開発機能</li>
<li><strong>生成AIアプリケーション開発</strong>： Amazon Bedrockを利用した生成AIアプリケーション開発機能</li>
</ul>
<h2 id="環境構築">環境構築</h2><p>これまで記載した内容を見るといろいろなサービスが関連していて環境準備がめんどくさそうだなと思われるかもしれませんが、動かすまでのステップはシンプルです（もちろんそれなりに深く使い込んでいく場合はハマりポイントが諸々あります。その辺は後述します）</p>
<ol>
<li><p>マネジメントコンソールからAmazon SageMakerのサービスに移動してドメイン作成しましょう</p>
<img src="/images/2025/20250527a/image.png" alt="image.png" width="1200" height="440" loading="lazy">
</li>
<li><p>とりあえず動かすだけであれば何も考えずに既存のVPCを使ってQuick Setupしちゃいましょう</p>
<img src="/images/2025/20250527a/image_2.png" alt="image.png" width="1200" height="546" loading="lazy">
</li>
<li><p>少し（数秒から数十秒）待てばドメインが作成されますね</p>
<img src="/images/2025/20250527a/image_3.png" alt="image.png" width="598" height="564" loading="lazy">
</li>
<li><p>この後は統合スタジオからデータの活用に向けたSageMakerのセットアップをしていく事になります</p>
<img src="/images/2025/20250527a/{8B87197D-E9A8-48E1-9190-50A4BB050BBF}.png" alt="{8B87197D-E9A8-48E1-9190-50A4BB050BBF}.png" width="1200" height="611" loading="lazy">

<p>ここで1つポイントですがこのままでは統合スタジオにはログインできないので、ログイン前に先ほど作成したドメインよりユーザーの追加してください（SSOユーザー、IAMユーザー、SSOグループのいずれかが追加できます）</p>
<img src="/images/2025/20250527a/image_4.png" alt="image.png" width="1200" height="472" loading="lazy">
</li>
<li><p>Good evening　これで統合スタジオにログインできるようになります</p>
<p>あとはニーズに合わせて統合スタジオ上でリソースやオブジェクトを作成していって頂けるといいかと思います</p>
<img src="/images/2025/20250527a/image_5.png" alt="image.png" width="1200" height="585" loading="lazy"></li>
</ol>
<h2 id="基本的な使い方">基本的な使い方</h2><p>この記事は初めてシリーズという事で個別の機能には触れませんが、上述した機能が使用できますので、必要に応じて機能を使っていって頂けるといいかと思います。</p>
<p>また、この製品の活用ポイントしては、いわゆるデータレイクに蓄積したデータをカタログ化してそのまま生成AIと連携させるなどデータ蓄積された状態からデータ活用に向けてシームレスに連携しやすくなるのがポイントではないかと思います。</p>
<p>SnowflakeやDatabricksなど外部のDWHにも対応しているので、データメッシュ構成の場合でも要件を満たす場合は利用が検討できるでしょう。</p>
<h2 id="利用におけるハマりポイントや今後改善を期待するポイント">利用におけるハマりポイントや今後改善を期待するポイント</h2><p>Amazon SageMaker Unified Studioにはデータ活用に関する様々な機能が実装されていますが2025年3月13日に一般提供されたばかりですのでハマりポイントや改善を期待したいポイントがあります（以下の記載内容は2025年5月23日時点の所感です）</p>
<ul>
<li>マニュアルの記載が乏しい部分がある（応用的な使い方をしようとするとマニュアルの記載内容では情報が足りない場合が多い）<ul>
<li>カタログに登録したデータの公開&#x2F;非公開によりどのように共有範囲が変わるのか？または挙動が変わるのか？</li>
<li>カタログに登録したデータの検索仕様など（何故これが検索結果にリストされるのかなど）</li>
<li>どこからどこまでがSagemakerの機能でどこからが別サービスの機能になるのか</li>
<li>結構な頻度で定期的に実行される処理がSagemaker上にあるがそれが何なのか？</li>
</ul>
</li>
<li>Terraformに対応していない（使い込んでいくと結構細かい設定が増えていくのでIaC化できないとつらい）</li>
<li>統合スタジオのUIが玄人向き（利用者のスキルレベルに合わせて画面カスタマイズできるとよい）</li>
<li>外部DWHとの連携で一部想定と異なる挙動あり（仕様通りだと思うがそういう動きを期待している訳ではないという事がある）</li>
<li>想定通りの挙動となった場合のエラー切り分けや対処が難しい（事例や情報が少ないため）</li>
<li>Amazon SageMaker Unified Studioからデータソースに対してクエリ実行できるが、外部のデータソースに対するレスポンスが悪い（コネクションを張った後、クエリ実行まで時間がかかるイメージ）</li>
<li>AWSの他サービスとの連携が基本になるのでIAMロールの設計や権限管理が難しい（特にGlueと連携する場合はLake Formationのハイブリッドモードを理解しないと想定通りのアクセスができない）</li>
</ul>
<h2 id="まとめ">まとめ</h2><p>いかがでしたでしょうか？ まだ一般提供されたばかりで今後の改善や発展を期待しますが非常にポテンシャルのあるサービスだと思いますので今後も継続的にウォッチしていきたいと思います。</p>
<p>またAWS以外にも似たような機能カバレッジを持つクラウドサービスやSaaSがあるので競合製品の動向も気にしつつ最適なプロダクトを選定していければと思っています。</p>
]]></content>
    <summary type="html">カタログ管理やデータ活用を行うツールとしてAmazon SageMaker Unified Studioを調査・検証したので触ってみた所感やポイントなどつらつらとこの記事に書いていこうと思います。</summary>
    <category term="DataEngineering" scheme="https://future-architect.github.io/categories/DataEngineering/"/>
    <category term="AWS" scheme="https://future-architect.github.io/tags/AWS/"/>
    <category term="SageMaker" scheme="https://future-architect.github.io/tags/SageMaker/"/>
    <category term="データカタログ" scheme="https://future-architect.github.io/tags/%E3%83%87%E3%83%BC%E3%82%BF%E3%82%AB%E3%82%BF%E3%83%AD%E3%82%B0/"/>
    <category term="データマネジメント" scheme="https://future-architect.github.io/tags/%E3%83%87%E3%83%BC%E3%82%BF%E3%83%9E%E3%83%8D%E3%82%B8%E3%83%A1%E3%83%B3%E3%83%88/"/>
  </entry>
  <entry>
    <title>初めてのMicrosoft Purview統合カタログ</title>
    <link href="https://future-architect.github.io/articles/20250526a/"/>
    <id>https://future-architect.github.io/articles/20250526a/</id>
    <published>2025-05-25T15:00:00.000Z</published>
    <updated>2025-05-25T15:00:00.000Z</updated>
    <author><name>佐々木伸悟</name></author>
    <content type="html"><![CDATA[<p>春の入門祭り2025の23本目の記事です。</p>
<h2 id="はじめに">はじめに</h2><p>はじめまして、佐々木です。</p>
<p>業務でデータカタログを調査することになり、その1つとしてMicrosoft Purviewについて調査しました。</p>
<p>その際に前もって知っていれば理解が早かったなと思ったことや触ってみて気になった箇所をまとめておきます。</p>
<h2 id="データカタログ-とは">データカタログ とは</h2><p>まず前提となるデータカタログについて説明します。</p>
<p>データカタログとは企業や組織のデータを一覧化し、可視化するツールです。テーブルのスキーマ情報といったテクニカルメタデータを自動収集したり、業務においてこのデータはどういった意味を表すかというビジネスメタデータを登録することで、検索性、可視性を高めて、組織のデータ利活用を促進させるためのものとなります。</p>
<p>現在においてはメタデータ管理やデータ探索の機能だけでなく、データライフサイクルを表すデータリネージのグラフ化機能、データの欠損値、整合性チェックを行うプロファイリングの機能、データ提供者・利用者間のコミュニケーションを行うワークフローの機能を持つことも多いです。</p>
<p>Collibra, Atlan, AlationといったSaaS、OpenMetadata, DataHubといったOSS製品もありますが、各クラウドベンダーもデータカタログに関連したサービスを出してます。</p>
<p>その中でAzureの提供するものがMicrosoft Purviewになります。</p>
<h2 id="Microsoft-Purviewとは">Microsoft Purviewとは</h2><p>Microsoft PurviewとはData Security, Data Governance, Risk &amp; Complianceにおけるソリューションを統合したサービスです。</p>
<img fetchpriority="high" src="/images/2025/20250526a/image.png" alt="PurviewはData Security、Data Govenance、Rist & Complianceの機能を持つ" width="1001" height="582">

<p>引用: Microsoft Purview の詳細</p>
<p>あくまでも一例になりますが、それぞれ下記のような機能を持ちます。</p>
<ul>
<li><strong>Data Security</strong><ul>
<li>情報保護: データ分類、暗号化、アクセス制限</li>
<li>データ損失防止: データ持ち出しの防止</li>
</ul>
</li>
<li><strong>Data Governance</strong><ul>
<li>データマップ: データソースのスキャン、可視化</li>
<li>データカタログ: データの検索、発見、理解</li>
<li>データ品質: データの完全性、整合性の評価、レポート</li>
</ul>
</li>
<li><strong>Risk &amp; Compliance</strong><ul>
<li>監査: アクティビティログの記録、不正の検出</li>
<li>コミュニケーションコンプライアンス: Outlook、Teamsなどのコミュニケーション内容の監視</li>
</ul>
</li>
</ul>
<p>PurviewにおいてデータカタログはData Governanceの1つのソリューションとして位置付けられており、データマップを作り、それを利用してデータカタログを作ることができます。</p>
<p>Purviewを調べる際、最初に「5分でわかるMicrosoft Purview」というYouTube動画を見て1回もデータカタログに関する話が出てこなくて困惑しました。しかしPurviewの全体像を理解することで腑に落ちました。</p>
<p>下の画像はPurviewポータルにおけるソリューション一覧ですが、データカタログは「データガバナンス」の1機能となっていることが理解できると思います。</p>
<img src="/images/2025/20250526a/image_2.png" alt="image.png" width="1200" height="866" loading="lazy">

<p>料金体系も最初見た時はわからなかったのですが、ここまで理解すると、データカタログに関するものは、「データのガバナンス」タブを見れば良いとわかります。</p>
<img src="/images/2025/20250526a/image_3.png" alt="image.png" width="1200" height="904" loading="lazy">

<p>引用: 価格 - Azure Purview | Microsoft Azure</p>
<h2 id="Azure-のデータカタログの歴史">Azure のデータカタログの歴史</h2><p>Microsoft Purviewのデータカタログを調べると、Azure Purviewという名前が見受けられます（上記で記載した料金のリンクもタイトルタグがAzure Purviewになっています）。</p>
<p>インターネットを漁ってみると、Azureの提供するデータカタログ製品は下記のような変遷をたどっているようです。</p>
<ul>
<li>Azure Data Catalog</li>
<li>Azure Purview<ul>
<li>Azure Data Catalog Gen 2として作られるも新名称に</li>
</ul>
</li>
<li>Microsoft Purview<ul>
<li>Microsoft 365 コンプライアンスと統合</li>
<li>旧ポータル。公式ドキュメントにおける「クラシック」</li>
</ul>
</li>
<li>Microsoft Purview<ul>
<li>新ポータル</li>
</ul>
</li>
</ul>
<img src="/images/2025/20250526a/image_4.png" alt="image.png" width="1200" height="822" loading="lazy">

<p>Azure PurviewがGAされたのは2021年となっており、ここ数年でも提供サービスや課金体系も大きく変わっていて進化が早いサービスとなります。</p>
<p>よって古い記事は参考程度に見るのが良さそうです。</p>
<p>また検索から入ってくるとクラシックに関する記載だったということもよくあるので、注意が必要です。</p>
<h2 id="触ってみて気になったこと">触ってみて気になったこと</h2><h3 id="機能の改廃">機能の改廃</h3><p>Purviewは進化途中のサービスであり、機能の入れ替えも行われています。</p>
<p>例えば新ポータルにおける統合カタログでは下記の機能がクラシックとして扱われ、後方互換のために残されているように見えます。</p>
<ul>
<li>用語集</li>
<li>ビジネス資産</li>
<li>資産の種類</li>
<li>マネージド属性</li>
</ul>
<img src="/images/2025/20250526a/image_5.png" alt="image.png" width="888" height="525" loading="lazy">

<p>ここの「用語集」は、その左のメニューにある「エンタープライズ用語集」と異なるものです。クラシックの「用語集」はデータ資産（テーブル、ファイルなど）とそのカラムに設定するものである一方、「エンタープライズ用語集」はデータ製品（データセットなど）に付与するものとなります。</p>
<p>今後このクラシックの機能がどうなるかはわかりませんが、使用していた機能が非推奨となる、廃止されるといったことはこれからも起きることが予想されます。</p>
<h3 id="マネージド属性における制限">マネージド属性における制限</h3><p>先の章で言及したマネージド属性も気になる点がありました。</p>
<p>マネージド属性とはデータ資産に付与できるいわゆるビジネスメタデータであり、key-valueの形式で情報を持つことができます。またバリューの持てる型（テキスト、日付）などを定義でき、必須項目かどうかを設定が可能です。</p>
<p>ただし下記のような制限事項があります。</p>
<ul>
<li>キー名に日本語を使用できない</li>
<li>関連づける資産の種類（AWS S3 バケット、Azure Blob Storageなど）でマネージド属性を変えることは出来るが、同一の種類の資産では同一のマネージド属性となる<ul>
<li>例えばAチームのS3バケットに対してはデータオーナーは必須としたいが、Bチームでは必須としたくないなど</li>
</ul>
</li>
<li>データ製品に対しては設定できない</li>
<li>属性グループの作成時にのみ必須項目とでき、あとから追加で必須に変更できない</li>
</ul>
<p>ドキュメントにも一部記載がありますが、実際の運用に載せる際には考慮が必要になりそうです。</p>
<ul>
<li>Microsoft Purview データ マップのマネージド属性 | Microsoft Learn</li>
</ul>
<h3 id="ロールが複雑">ロールが複雑</h3><p>Purviewの中でもPurviewソリューションレベルのロール、データマップのドメイン・コレクションレベルのロール、統合カタログのガバナンスドメインレベルのロールと様々なロールがあり、かなり柔軟に権限を設定できます。</p>
<p>設定可能箇所が多いため各チームで必要な権限を明確にし、必要な権限のみを与えるよう設計を丁寧に行う必要があると思いました。</p>
<h2 id="まとめ">まとめ</h2><p>Microsoft Purviewにおけるデータカタログについて概要と歴史、また気になったことを説明してきました。</p>
<p>具体的な機能についてはあまり記載できませんでしたが、長くなってしまうため他の記事の紹介に留めておこうと思います。</p>
<p>データカタログを触ってみた記事。旧ポータルだが、イメージはわかると思います。</p>
<ul>
<li>Microsoft Purviewのデータカタログ・データリネージ機能を検証してみる | Azure導入支援デスク</li>
</ul>
<p>新しいPurviewにおけるData Governanceを説明するスライド。詳細機能も記載されており参考になりました。</p>
<ul>
<li>Microsoft Purview Data Governance について - Speaker Deck</li>
</ul>
<p>ではでは。</p>
<h2 id="参考">参考</h2><ul>
<li>Microsoft Purview の詳細 | Microsoft Learn</li>
<li>Azure 統合カタログ (ADC) Gen 2、Azure Information Protection、Microsoft Purview (旧称 Azure Purview) はどのように関連していますか? | Microsoft Purview データ ガバナンス ソリューションについてよく寄せられる質問 | Microsoft Learn</li>
</ul>
]]></content>
    <summary type="html">データカタログを調査することになり、その一つとしてMicrosoft Purviewについて調査を行いました。その際に前もって知っていれば理解が早かったなと思ったことや触ってみて気になった箇所をまとめておきます。</summary>
    <category term="DataEngineering" scheme="https://future-architect.github.io/categories/DataEngineering/"/>
    <category term="Azure" scheme="https://future-architect.github.io/tags/Azure/"/>
    <category term="データカタログ" scheme="https://future-architect.github.io/tags/%E3%83%87%E3%83%BC%E3%82%BF%E3%82%AB%E3%82%BF%E3%83%AD%E3%82%B0/"/>
    <category term="データマネジメント" scheme="https://future-architect.github.io/tags/%E3%83%87%E3%83%BC%E3%82%BF%E3%83%9E%E3%83%8D%E3%82%B8%E3%83%A1%E3%83%B3%E3%83%88/"/>
  </entry>
  <entry>
    <title>dbt Core × BigQueryを使ったデータ変換をやってみた</title>
    <link href="https://future-architect.github.io/articles/20250515a/"/>
    <id>https://future-architect.github.io/articles/20250515a/</id>
    <published>2025-05-14T15:00:00.000Z</published>
    <updated>2025-05-14T15:00:00.000Z</updated>
    <author><name>片岡久人</name></author>
    <content type="html"><![CDATA[<img fetchpriority="high" src="/images/2025/20250515a/top.png" alt="" width="446" height="162">

<p>春の入門祭り2025 16本目の記事です。</p>
<h2 id="はじめに">はじめに</h2><p>はじめまして、フューチャーアーキテクト、製造エネルギー事業部の片岡です。</p>
<p>データ分析基盤の構築や運用において注目を集めている dbt (data build tool) の入門記事です。dbt を活用して、データ変換の一連の手順を示すことで、これから dbt を試してみようと考えている方の導入を支援できれば幸いです。</p>
<h2 id="dbt-とは">dbt とは</h2><p>dbt (data build tool) は、dbt Labs, Inc. が提供するデータ変換ツールです。SQL をコンパイルして実行することで、データウェアハウスにテーブルやビューを作成できます。</p>
<p>dbt には以下の 2 種類のプロダクトがあります。</p>
<ul>
<li>dbt Cloud: Web ベースの UI を通じて、開発、テスト、スケジュール実行などを行うフルマネージドサービス</li>
<li>dbt Core: コマンドラインを通じてインストール・管理するオープンソースツール</li>
</ul>
<p>今回の記事では、dbt Core を利用します。</p>
<h2 id="やりたいこと">やりたいこと</h2><ul>
<li>dbt を利用して、BigQuery に登録したデータの変換処理を実現します</li>
<li>具体的には、下記のサンプルで用意した店舗の売上データを、dbt を利用して分析してみようと思います</li>
</ul>
<div class="scroll"><table>
<thead>
<tr>
<th>店舗 ID</th>
<th>商品 ID</th>
<th>売上日</th>
<th>売上個数</th>
<th>売上金額</th>
<th>顧客 ID</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>1</td>
<td>2023&#x2F;1&#x2F;1</td>
<td>10</td>
<td>1000</td>
<td>1</td>
</tr>
<tr>
<td>1</td>
<td>2</td>
<td>2023&#x2F;1&#x2F;1</td>
<td>5</td>
<td>500</td>
<td>2</td>
</tr>
<tr>
<td>1</td>
<td>3</td>
<td>2023&#x2F;1&#x2F;2</td>
<td>20</td>
<td>2000</td>
<td>3</td>
</tr>
<tr>
<td>2</td>
<td>1</td>
<td>2023&#x2F;1&#x2F;1</td>
<td>15</td>
<td>1500</td>
<td>4</td>
</tr>
<tr>
<td>2</td>
<td>2</td>
<td>2023&#x2F;1&#x2F;1</td>
<td>10</td>
<td>1000</td>
<td>1</td>
</tr>
<tr>
<td>2</td>
<td>3</td>
<td>2023&#x2F;1&#x2F;2</td>
<td>30</td>
<td>3000</td>
<td>5</td>
</tr>
<tr>
<td>3</td>
<td>1</td>
<td>2023&#x2F;1&#x2F;1</td>
<td>2</td>
<td>200</td>
<td>6</td>
</tr>
<tr>
<td>3</td>
<td>2</td>
<td>2023&#x2F;1&#x2F;2</td>
<td>12</td>
<td>1200</td>
<td>7</td>
</tr>
<tr>
<td>3</td>
<td>3</td>
<td>2023&#x2F;1&#x2F;2</td>
<td>50</td>
<td>5000</td>
<td>8</td>
</tr>
</tbody></table></div>
<h2 id="環境構築">環境構築</h2><p>まずは、dbt を利用するために環境構築をします。</p>
<p>（公式の環境構築ガイドはこちらです）</p>
<h3 id="前提条件">前提条件</h3><ul>
<li>Python 3.9 以上がインストールされていること</li>
<li>Google Cloud のプロジェクトが作成されていること</li>
<li>今回は dbt に接続するプラットフォームとして、BigQuery を利用します</li>
</ul>
<h3 id="dbt-を-install-する">dbt を install する</h3><p>pip コマンドを用いて、dbt Core と BigQuery アダプターをインストールします。</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">python -m pip install dbt-core dbt-bigquery</span><br><span class="line"></span><br><span class="line"><span class="comment">## 下記のコマンドを実行し、versionが表示されればインストール成功</span></span><br><span class="line">dbt --version</span><br></pre></td></tr></table></figure>

<h3 id="dbt-プロジェクトを作成する">dbt プロジェクトを作成する</h3><p><code>dbt init</code>コマンドを実行し、dbt プロジェクトフォルダを作成します。</p>
<p>今回は dbt_trial という名前のプロジェクトを作成します。</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="comment"># dbt init &lt;プロジェクト名&gt;</span></span><br><span class="line">dbt init dbt_trial</span><br><span class="line"></span><br><span class="line"><span class="comment"># dbtプロジェクト作成にあたり、下記のような設定をCLI上で実施します。</span></span><br><span class="line">Which database would you like to use?</span><br><span class="line">[1] bigquery</span><br><span class="line">Enter a number: 1</span><br><span class="line">[1] oauth</span><br><span class="line">[2] service_account</span><br><span class="line">Desired authentication method option (enter a number): 1</span><br><span class="line">project (GCP project <span class="built_in">id</span>): sample_project</span><br><span class="line">dataset (the name of your dbt dataset): sample</span><br><span class="line">threads (1 or more): 1</span><br><span class="line">job_execution_timeout_seconds [300]: 300</span><br><span class="line">[1] US</span><br><span class="line">[2] EU</span><br><span class="line">Desired location option (enter a number): 1</span><br><span class="line">07:02:41  Profile dbt_trial written to C:\Users\&lt;user名&gt;\.dbt\profiles.yml using target<span class="string">&#x27;s profile_template.yml and your supplied values. Run &#x27;</span>dbt debug<span class="string">&#x27; to validate the connection.</span></span><br><span class="line"><span class="string"></span></span><br></pre></td></tr></table></figure>

<div class="note-container note-info"><span class="note-icon"></span><div>

<p>補足: プロジェクト名やデータベースの設定は、環境に合わせて適切に選択してください。</p>
</div></div>

<h3 id="BigQuery-に接続する">BigQuery に接続する</h3><p>dbt プロジェクトが作成されると、<code>~/.dbt/</code>ディレクトリに profiles.yml が作成されます。このファイルに、dbt が BigQuery に接続するための認証情報などを記述します。</p>
<p>もし作成されていない場合は、<code>~/.dbt/</code>ディレクトリを作成し、下記のファイルをコピーしてください。</p>
<figure class="highlight yml"><figcaption><span>profiles.yml</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="attr">dbt_trial:</span></span><br><span class="line">  <span class="attr">outputs:</span></span><br><span class="line">    <span class="attr">dev:</span></span><br><span class="line">      <span class="attr">dataset:</span> <span class="string">trial</span></span><br><span class="line">      <span class="attr">job_execution_timeout_seconds:</span> <span class="number">300</span></span><br><span class="line">      <span class="attr">job_retries:</span> <span class="number">1</span></span><br><span class="line">      <span class="attr">location:</span> <span class="string">US</span></span><br><span class="line">      <span class="attr">method:</span> <span class="string">oauth</span></span><br><span class="line">      <span class="attr">priority:</span> <span class="string">interactive</span></span><br><span class="line">      <span class="attr">project:</span> <span class="string">sample_project</span></span><br><span class="line">      <span class="attr">threads:</span> <span class="number">1</span></span><br><span class="line">      <span class="attr">type:</span> <span class="string">bigquery</span></span><br><span class="line">  <span class="attr">target:</span> <span class="string">dev</span></span><br></pre></td></tr></table></figure>

<div class="note-container note-warn"><span class="note-icon"></span><div>

<p>注意: project にはご自身の GCP プロジェクト ID を、dataset には dbt でテーブルを作成する BigQuery のデータセット名を指定してください。location はデータセットのロケーションに合わせてください。</p>
</div></div>

<p>作成した dbt プロジェクトフォルダの直下で<code>dbt debug</code>コマンドを実行し、BigQuery への接続が正常に確立できるかを確認します。</p>
<figure class="highlight console"><table><tr><td class="code"><pre><span class="line">dbt debug</span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">接続に成功すると、以下のような出力が表示されます。</span></span><br><span class="line"><span class="meta prompt_">&gt; </span><span class="language-bash">Connection <span class="built_in">test</span>: OK connection ok</span></span><br></pre></td></tr></table></figure>

<h3 id="dbt-のサンプルのモデルを作成してみる">dbt のサンプルのモデルを作成してみる</h3><p>作成した dbt プロジェクトには、初期状態ではシンプルなサンプルモデルが含まれています。以下のコマンドを実行して、このサンプルモデルが正常に実行できるか試してみましょう。</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">dbt run</span><br></pre></td></tr></table></figure>

<p>以下のように、サンプルのモデルが BigQuery 上に作成されたら、dbt の基本的な環境構築は完了です！<br><img src="/images/2025/20250515a/image.png" alt="image.png" width="537" height="265" loading="lazy"></p>
<h2 id="dbt-を利用してデータ変換を実行してみる。">dbt を利用してデータ変換を実行してみる。</h2><p>環境構築ができたので、早速データ変換を実施してみようと思います。</p>
<ul>
<li>本記事では、売り上げデータの分析として、日付ごとの売上の合計を取得してみようと思います。</li>
<li>実装にあたり、dbt にはレイヤという考え方があり、レイヤの作成方針のベストプラクティスに倣ってデータ変換処理を実装していきます。</li>
</ul>
<div class="note-container note-info"><span class="note-icon"></span><div>

<p><strong>dbt のレイヤの考え方</strong></p>
<p>dbt におけるレイヤは、データの変換プロセスを整理し、管理しやすくするための重要な概念です。以下のようなレイヤ構成がベストプラクティスとされています。</p>
<ul>
<li>Staging レイヤ: データソースからロードされた生データに対して、データ型を整えたり、カラム名を分かりやすいように変更するなど、最低限の変換をします。このレイヤのモデルは、通常、データソースの構造を反映した形になります</li>
<li>Intermediate レイヤ : staging レイヤのデータを基に、複数のテーブルを結合したり、ビジネスロジックに基づいた集計や計算をしたりするなど、より複雑な変換をします。このレイヤは、分析に必要な基本的なデータセットを準備する役割を担います。</li>
<li>Mart レイヤ: Staging・Intermediate レイヤで準備されたデータセットを、特定の分析目的やレポート作成に合わせてさらに集計・加工し、最終的な分析に利用しやすい形に整理します。例えば、日次の売上集計、顧客ごとの購買履歴などがこのレイヤに作成されます。</li>
</ul>
</div></div>

<p>下記のような手順でデータ変換を実施していきます。</p>
<ol>
<li>CSV で保存されている売上データを BigQuery に投入</li>
<li>staging レイヤのモデルを作成</li>
<li>mart レイヤにて日付ごとの売り上げを合計を取得するモデルを作成</li>
</ol>
<h3 id="CSV-で保存されている売上データを-BigQuery-に投入">CSV で保存されている売上データを BigQuery に投入</h3><p>こちらは、データ変換ではないのですが、データ変換のもとなるデータを BiqQuery に投入する手順となります。</p>
<p>以下の CSV ファイルを、dbt プロジェクト内の dbt_trial&#x2F;seeds&#x2F; ディレクトリ配下にコピー&amp;ペーストします。</p>
<div class="note-container note-warn"><span class="note-icon"></span><div>

<p>今回は sales.csv のみ利用します。<br>それ以外のデータはデータ分析用のサンプルデータとしてご利用ください。（生成 AI を利用して作成しております。）</p>
</div></div>

<details><summary>sales.csv（店舗の売り上げデータ）</summary>

<figure class="highlight csv"><figcaption><span>sales.csv</span></figcaption><table><tbody><tr><td class="code"><pre><span class="line"><span class="csv-col-1">store_id</span><span class="csv-comma">,</span><span class="csv-col-2">item_id</span><span class="csv-comma">,</span><span class="csv-col-3">sales_date</span><span class="csv-comma">,</span><span class="csv-col-4">sales_quantity</span><span class="csv-comma">,</span><span class="csv-col-5">sales_amount</span><span class="csv-comma">,</span><span class="csv-col-6">customer_id</span></span><br><span class="line"><span class="csv-col-1">1</span><span class="csv-comma">,</span><span class="csv-col-2">1</span><span class="csv-comma">,</span><span class="csv-col-3">2023-01-01</span><span class="csv-comma">,</span><span class="csv-col-4">10</span><span class="csv-comma">,</span><span class="csv-col-5">1000</span><span class="csv-comma">,</span><span class="csv-col-6">1</span></span><br><span class="line"><span class="csv-col-1">1</span><span class="csv-comma">,</span><span class="csv-col-2">2</span><span class="csv-comma">,</span><span class="csv-col-3">2023-01-01</span><span class="csv-comma">,</span><span class="csv-col-4">5</span><span class="csv-comma">,</span><span class="csv-col-5">500</span><span class="csv-comma">,</span><span class="csv-col-6">2</span></span><br><span class="line"><span class="csv-col-1">1</span><span class="csv-comma">,</span><span class="csv-col-2">3</span><span class="csv-comma">,</span><span class="csv-col-3">2023-01-02</span><span class="csv-comma">,</span><span class="csv-col-4">20</span><span class="csv-comma">,</span><span class="csv-col-5">2000</span><span class="csv-comma">,</span><span class="csv-col-6">3</span></span><br><span class="line"><span class="csv-col-1">2</span><span class="csv-comma">,</span><span class="csv-col-2">1</span><span class="csv-comma">,</span><span class="csv-col-3">2023-01-01</span><span class="csv-comma">,</span><span class="csv-col-4">15</span><span class="csv-comma">,</span><span class="csv-col-5">1500</span><span class="csv-comma">,</span><span class="csv-col-6">4</span></span><br><span class="line"><span class="csv-col-1">2</span><span class="csv-comma">,</span><span class="csv-col-2">2</span><span class="csv-comma">,</span><span class="csv-col-3">2023-01-01</span><span class="csv-comma">,</span><span class="csv-col-4">10</span><span class="csv-comma">,</span><span class="csv-col-5">1000</span><span class="csv-comma">,</span><span class="csv-col-6">1</span></span><br><span class="line"><span class="csv-col-1">2</span><span class="csv-comma">,</span><span class="csv-col-2">3</span><span class="csv-comma">,</span><span class="csv-col-3">2023-01-02</span><span class="csv-comma">,</span><span class="csv-col-4">30</span><span class="csv-comma">,</span><span class="csv-col-5">3000</span><span class="csv-comma">,</span><span class="csv-col-6">5</span></span><br><span class="line"><span class="csv-col-1">3</span><span class="csv-comma">,</span><span class="csv-col-2">1</span><span class="csv-comma">,</span><span class="csv-col-3">2023-01-01</span><span class="csv-comma">,</span><span class="csv-col-4">2</span><span class="csv-comma">,</span><span class="csv-col-5">200</span><span class="csv-comma">,</span><span class="csv-col-6">6</span></span><br><span class="line"><span class="csv-col-1">3</span><span class="csv-comma">,</span><span class="csv-col-2">2</span><span class="csv-comma">,</span><span class="csv-col-3">2023-01-02</span><span class="csv-comma">,</span><span class="csv-col-4">12</span><span class="csv-comma">,</span><span class="csv-col-5">1200</span><span class="csv-comma">,</span><span class="csv-col-6">7</span></span><br><span class="line"><span class="csv-col-1">3</span><span class="csv-comma">,</span><span class="csv-col-2">3</span><span class="csv-comma">,</span><span class="csv-col-3">2023-01-02</span><span class="csv-comma">,</span><span class="csv-col-4">50</span><span class="csv-comma">,</span><span class="csv-col-5">5000</span><span class="csv-comma">,</span><span class="csv-col-6">8</span></span><br></pre></td></tr></tbody></table></figure>

</details>

<details><summary>store.csv（店舗のマスタデータ）</summary>

<figure class="highlight csv"><figcaption><span>store.csv</span></figcaption><table><tbody><tr><td class="code"><pre><span class="line"><span class="csv-col-1">store_id</span><span class="csv-comma">,</span><span class="csv-col-2">store_name</span><span class="csv-comma">,</span><span class="csv-col-3">address</span><span class="csv-comma">,</span><span class="csv-col-4">latitude</span><span class="csv-comma">,</span><span class="csv-col-5">longitude</span></span><br><span class="line"><span class="csv-col-1">1</span><span class="csv-comma">,</span><span class="csv-col-2">渋谷店</span><span class="csv-comma">,</span><span class="csv-col-3">〒150-0041 東京都渋谷区宇田川町21-1</span><span class="csv-comma">,</span><span class="csv-col-4">35.662863</span><span class="csv-comma">,</span><span class="csv-col-5">139.700983</span></span><br><span class="line"><span class="csv-col-1">2</span><span class="csv-comma">,</span><span class="csv-col-2">新宿店</span><span class="csv-comma">,</span><span class="csv-col-3">〒160-0022 東京都新宿区新宿3-15-1</span><span class="csv-comma">,</span><span class="csv-col-4">35.689540</span><span class="csv-comma">,</span><span class="csv-col-5">139.701351</span></span><br><span class="line"><span class="csv-col-1">3</span><span class="csv-comma">,</span><span class="csv-col-2">池袋店</span><span class="csv-comma">,</span><span class="csv-col-3">〒170-0004 東京都豊島区西池袋1-39-1</span><span class="csv-comma">,</span><span class="csv-col-4">35.727756</span><span class="csv-comma">,</span><span class="csv-col-5">139.700242</span></span><br></pre></td></tr></tbody></table></figure>

</details>

<details><summary>item.csv（商品のマスタデータ）</summary>

<figure class="highlight csv"><figcaption><span>item.csv</span></figcaption><table><tbody><tr><td class="code"><pre><span class="line"><span class="csv-col-1">item_id</span><span class="csv-comma">,</span><span class="csv-col-2">item_name</span><span class="csv-comma">,</span><span class="csv-col-3">category</span><span class="csv-comma">,</span><span class="csv-col-4">unit_price</span></span><br><span class="line"><span class="csv-col-1">1</span><span class="csv-comma">,</span><span class="csv-col-2">Tシャツ</span><span class="csv-comma">,</span><span class="csv-col-3">服飾</span><span class="csv-comma">,</span><span class="csv-col-4">1000</span></span><br><span class="line"><span class="csv-col-1">2</span><span class="csv-comma">,</span><span class="csv-col-2">パンツ</span><span class="csv-comma">,</span><span class="csv-col-3">服飾</span><span class="csv-comma">,</span><span class="csv-col-4">2000</span></span><br><span class="line"><span class="csv-col-1">3</span><span class="csv-comma">,</span><span class="csv-col-2">スニーカー</span><span class="csv-comma">,</span><span class="csv-col-3">靴</span><span class="csv-comma">,</span><span class="csv-col-4">5000</span></span><br><span class="line"><span class="csv-col-1">4</span><span class="csv-comma">,</span><span class="csv-col-2">シャンプー</span><span class="csv-comma">,</span><span class="csv-col-3">日用品</span><span class="csv-comma">,</span><span class="csv-col-4">500</span></span><br><span class="line"><span class="csv-col-1">5</span><span class="csv-comma">,</span><span class="csv-col-2">洗剤</span><span class="csv-comma">,</span><span class="csv-col-3">日用品</span><span class="csv-comma">,</span><span class="csv-col-4">300</span></span><br></pre></td></tr></tbody></table></figure>

</details>

<details><summary>customer.csv（顧客のマスタデータ）</summary>

<figure class="highlight csv"><figcaption><span>customer.csv</span></figcaption><table><tbody><tr><td class="code"><pre><span class="line"><span class="csv-col-1">customer_id</span><span class="csv-comma">,</span><span class="csv-col-2">customer_name</span><span class="csv-comma">,</span><span class="csv-col-3">address</span><span class="csv-comma">,</span><span class="csv-col-4">gender</span><span class="csv-comma">,</span><span class="csv-col-5">date_of_birth</span></span><br><span class="line"><span class="csv-col-1">1</span><span class="csv-comma">,</span><span class="csv-col-2">山田太郎</span><span class="csv-comma">,</span><span class="csv-col-3">〒100-0001 東京都千代田区千代田1-1-1</span><span class="csv-comma">,</span><span class="csv-col-4">男</span><span class="csv-comma">,</span><span class="csv-col-5">1990-01-01</span></span><br><span class="line"><span class="csv-col-1">2</span><span class="csv-comma">,</span><span class="csv-col-2">田中花子</span><span class="csv-comma">,</span><span class="csv-col-3">〒200-0002 東京都中野区中野1-1-1</span><span class="csv-comma">,</span><span class="csv-col-4">女</span><span class="csv-comma">,</span><span class="csv-col-5">1995-04-05</span></span><br><span class="line"><span class="csv-col-1">3</span><span class="csv-comma">,</span><span class="csv-col-2">佐藤健</span><span class="csv-comma">,</span><span class="csv-col-3">〒300-0003 東京都杉並区杉並1-1-1</span><span class="csv-comma">,</span><span class="csv-col-4">男</span><span class="csv-comma">,</span><span class="csv-col-5">2000-07-07</span></span><br><span class="line"><span class="csv-col-1">4</span><span class="csv-comma">,</span><span class="csv-col-2">鈴木美咲</span><span class="csv-comma">,</span><span class="csv-col-3">〒400-0004 東京都板橋区板橋1-1-1</span><span class="csv-comma">,</span><span class="csv-col-4">女</span><span class="csv-comma">,</span><span class="csv-col-5">2005-10-10</span></span><br><span class="line"><span class="csv-col-1">5</span><span class="csv-comma">,</span><span class="csv-col-2">高橋一郎</span><span class="csv-comma">,</span><span class="csv-col-3">〒500-0005 東京都練馬区練馬1-1-1</span><span class="csv-comma">,</span><span class="csv-col-4">男</span><span class="csv-comma">,</span><span class="csv-col-5">2010-01-12</span></span><br><span class="line"><span class="csv-col-1">6</span><span class="csv-comma">,</span><span class="csv-col-2">伊藤真琴</span><span class="csv-comma">,</span><span class="csv-col-3">〒600-0006 東京都足立区足立1-1-1</span><span class="csv-comma">,</span><span class="csv-col-4">女</span><span class="csv-comma">,</span><span class="csv-col-5">2015-02-14</span></span><br><span class="line"><span class="csv-col-1">7</span><span class="csv-comma">,</span><span class="csv-col-2">中村俊介</span><span class="csv-comma">,</span><span class="csv-col-3">〒700-0007 東京都葛飾区葛飾1-1-1</span><span class="csv-comma">,</span><span class="csv-col-4">男</span><span class="csv-comma">,</span><span class="csv-col-5">2020-03-16</span></span><br><span class="line"><span class="csv-col-1">8</span><span class="csv-comma">,</span><span class="csv-col-2">橋本美咲</span><span class="csv-comma">,</span><span class="csv-col-3">〒800-0008 東京都江戸川区江戸川1-1-1</span><span class="csv-comma">,</span><span class="csv-col-4">女</span><span class="csv-comma">,</span><span class="csv-col-5">2025-04-18</span></span><br></pre></td></tr></tbody></table></figure>

</details>

<p>作成した dbt プロジェクトの直下で<code>dbt seed</code>コマンドを実行して、これらの CSV データを BigQuery に投入します。dbt は seeds ディレクトリ内の CSV ファイルを BigQuery のテーブルとしてロードします。</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">dbt seed</span><br></pre></td></tr></table></figure>

<p>下記のようにテーブルが作成され、データがロードされていれば OK です。<br><img src="/images/2025/20250515a/image_2.png" alt="image.png" width="993" height="404" loading="lazy"></p>
<div class="note-container note-info"><span class="note-icon"></span><div>

<p>補足：dbt seed コマンドは、dbt プロジェクトの seeds ディレクトリに配置された CSV ファイルを、データウェアハウス（ここでは BigQuery）にテーブルとしてロードするためのコマンドです。主に、分析に必要な参照テーブルや、頻繁には変更されない比較的小さなデータセットを管理するのに適しています。</p>
</div></div>

<h3 id="staging-レイヤのモデルを作成する">staging レイヤのモデルを作成する</h3><ul>
<li>dbt_trial&#x2F;models 直下に staging ディレクトリを作成します。</li>
<li>作成した staging ディレクトリ配下に、以下の 3 つのファイルを新規作成し、それぞれの内容をコピー&amp;ペーストしてください。</li>
<li>本記事では、BigQuery に投入したデータソース（sales.csv）の内容を、そのまま staging レイヤのモデルとして利用します。</li>
</ul>
<details><summary>stg_sales.sql</summary>

<figure class="highlight sql"><figcaption><span>stg_sales.sql</span></figcaption><table><tr><td class="code"><pre><span class="line">&#123;&#123; config(schema<span class="operator">=</span><span class="string">&#x27;staging&#x27;</span>) &#125;&#125;</span><br><span class="line"><span class="keyword">with</span></span><br><span class="line"></span><br><span class="line">source <span class="keyword">as</span> (</span><br><span class="line">    <span class="keyword">select</span> <span class="operator">*</span> <span class="keyword">from</span> &#123;&#123; source(<span class="string">&#x27;trial&#x27;</span>,<span class="string">&#x27;sales&#x27;</span>) &#125;&#125;</span><br><span class="line">),</span><br><span class="line"></span><br><span class="line">renamed <span class="keyword">as</span> (</span><br><span class="line">    <span class="keyword">select</span></span><br><span class="line">        store_id,</span><br><span class="line">        item_id,</span><br><span class="line">        sales_date,</span><br><span class="line">        sales_quantity,</span><br><span class="line">        sales_amount,</span><br><span class="line">        customer_id</span><br><span class="line">    <span class="keyword">from</span></span><br><span class="line">        source</span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="keyword">select</span> <span class="operator">*</span> <span class="keyword">from</span> renamed</span><br></pre></td></tr></table></figure>

</details>

<details><summary>stg_sales.sql</summary>

<figure class="highlight yml"><figcaption><span>_stg__models.yml</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="attr">version:</span> <span class="number">2</span></span><br><span class="line"></span><br><span class="line"><span class="attr">models:</span></span><br><span class="line">  <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">stg_sales</span></span><br><span class="line">    <span class="attr">dataset:</span> <span class="string">staging</span></span><br><span class="line">    <span class="attr">description:</span> <span class="string">&quot;売上データ&quot;</span></span><br><span class="line">    <span class="attr">columns:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">store_id</span></span><br><span class="line">        <span class="attr">description:</span> <span class="string">&quot;店舗 ID&quot;</span></span><br><span class="line">        <span class="attr">tests:</span></span><br><span class="line">          <span class="bullet">-</span> <span class="string">not_null</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">item_id</span></span><br><span class="line">        <span class="attr">description:</span> <span class="string">&quot;商品 ID&quot;</span></span><br><span class="line">        <span class="attr">tests:</span></span><br><span class="line">          <span class="bullet">-</span> <span class="string">not_null</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">sales_date</span></span><br><span class="line">        <span class="attr">description:</span> <span class="string">&quot;売上日付&quot;</span></span><br><span class="line">        <span class="attr">tests:</span></span><br><span class="line">          <span class="bullet">-</span> <span class="string">not_null</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">sales_quantity</span></span><br><span class="line">        <span class="attr">description:</span> <span class="string">&quot;売上個数&quot;</span></span><br><span class="line">        <span class="attr">tests:</span></span><br><span class="line">          <span class="bullet">-</span> <span class="string">not_null</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">sales_amount</span></span><br><span class="line">        <span class="attr">description:</span> <span class="string">&quot;売上金額&quot;</span></span><br><span class="line">        <span class="attr">tests:</span></span><br><span class="line">          <span class="bullet">-</span> <span class="string">not_null</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">customer_id</span></span><br><span class="line">        <span class="attr">description:</span> <span class="string">&quot;顧客 ID&quot;</span></span><br><span class="line">        <span class="attr">tests:</span></span><br><span class="line">          <span class="bullet">-</span> <span class="string">not_null</span></span><br></pre></td></tr></table></figure>

</details>

<details><summary>_stg__sources.yml</summary>

<figure class="highlight yml"><figcaption><span>_stg__sources.yml</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="attr">sources:</span></span><br><span class="line">  <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">trial</span></span><br><span class="line">    <span class="attr">dataset:</span> <span class="string">trial</span></span><br><span class="line">    <span class="attr">tables:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">&quot;sales&quot;</span></span><br><span class="line">        <span class="attr">description:</span> <span class="string">&quot;システムXから連携されてきた売上マスタ&quot;</span></span><br></pre></td></tr></table></figure>

</details>

<p><code>dbt run</code>コマンドを実行し、stg_sales を作成します。</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">dbt run --<span class="keyword">select</span> <span class="string">&quot;dbt_trial.staging.stg_sales&quot;</span></span><br></pre></td></tr></table></figure>

<p>下記のような View テーブルが作成されます。<br><img src="/images/2025/20250515a/image_3.png" alt="image.png" width="766" height="392" loading="lazy"></p>
<div class="note-container note-info"><span class="note-icon"></span><div>

<p>stg_sales.sql などの SQL ファイルには、データの抽出、変換、ロード（ETL の T と L）のロジックを記述し、対応する YAML ファイル（_stgmodels.yml、_stgsources.yml）には、モデルのメタ情報（説明、カラム定義、テストなど）やデータソースの定義を記述します。</p>
</div></div>

<h3 id="mart-レイヤのモデルを作成する">mart レイヤのモデルを作成する</h3><ul>
<li>作成した dbt プロジェクトの models ディレクトリ内に、mart という名前のディレクトリを作成します。</li>
<li>作成した mart ディレクトリ配下に、以下の 2 つのファイルを新規作成し、それぞれの内容をコピー&amp;ペーストしてください。</li>
<li>mart_sales_amount_per_day では、stg_sales モデルを日付で集約し、日付毎の売り上げを取得するロジックを実装しています。</li>
</ul>
<details><summary>mart_sales_amount_per_day.sql</summary>

<figure class="highlight sql"><figcaption><span>mart_sales_amount_per_day.sql</span></figcaption><table><tr><td class="code"><pre><span class="line">&#123;&#123; config(schema<span class="operator">=</span><span class="string">&#x27;mart&#x27;</span>) &#125;&#125;</span><br><span class="line"><span class="keyword">with</span></span><br><span class="line"></span><br><span class="line">sales <span class="keyword">as</span> (<span class="keyword">select</span> <span class="operator">*</span> <span class="keyword">from</span> &#123;&#123; <span class="keyword">ref</span>(<span class="string">&#x27;stg_sales&#x27;</span>) &#125;&#125;),</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">sales_amount_per_day <span class="keyword">as</span> (</span><br><span class="line">    <span class="keyword">select</span></span><br><span class="line">        sales_date,</span><br><span class="line">        <span class="built_in">sum</span>(sales_amount) <span class="keyword">as</span> sales_amount_per_day</span><br><span class="line">    <span class="keyword">from</span></span><br><span class="line">        sales</span><br><span class="line">    <span class="keyword">group</span> <span class="keyword">by</span></span><br><span class="line">        sales_date</span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="keyword">select</span> <span class="operator">*</span> <span class="keyword">from</span> sales_amount_per_day</span><br></pre></td></tr></table></figure>

</details>

<details><summary>_marts__models.yml</summary>

<figure class="highlight yml"><figcaption><span>_marts__models.yml</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="attr">version:</span> <span class="number">2</span></span><br><span class="line"></span><br><span class="line"><span class="attr">models:</span></span><br><span class="line">  <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">mart_sales_amount_per_day</span></span><br><span class="line">    <span class="attr">dataset:</span> <span class="string">trial_mart</span></span><br><span class="line">    <span class="attr">description:</span> <span class="string">mart_sales_amount_per_day</span></span><br><span class="line">    <span class="attr">columns:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">sales_date</span></span><br><span class="line">        <span class="attr">description:</span> <span class="string">&quot;売上日付&quot;</span></span><br><span class="line">        <span class="attr">tests:</span></span><br><span class="line">          <span class="bullet">-</span> <span class="string">unique</span></span><br><span class="line">          <span class="bullet">-</span> <span class="string">not_null</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">sales_amount_per_day</span></span><br><span class="line">        <span class="attr">description:</span> <span class="string">&quot;日毎の売上金額&quot;</span></span><br><span class="line">        <span class="attr">tests:</span></span><br><span class="line">          <span class="bullet">-</span> <span class="string">not_null</span></span><br></pre></td></tr></table></figure>

</details>

<p>dbt run コマンドを実行し、mart_sales_amount_per_day を作成する。</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">dbt run --<span class="keyword">select</span> <span class="string">&quot;dbt_trial.mart.mart_sales_amount_per_day&quot;</span></span><br></pre></td></tr></table></figure>

<p>下記のような View テーブルが作成されます。</p>
<img src="/images/2025/20250515a/image_4.png" alt="" width="778" height="264" loading="lazy">

<p>View テーブルの中身を確認すると、日付毎に集約された売上金額の合計が取得できていることがわかります。</p>
<img src="/images/2025/20250515a/image_5.png" alt="" width="663" height="292" loading="lazy">

<p>これにて、本記事でやりたいことが実現できました！</p>
<h2 id="まとめ">まとめ</h2><p>今回の記事では、dbt Core を用いた簡単なデータ変換処理実装の流れを記載しました。</p>
<ul>
<li>dbt の環境構築から BigQuery への接続</li>
<li>CSV データの BigQuery への投入 (dbt seed)</li>
<li>staging レイヤと mart レイヤのモデル作成 (dbt run)</li>
</ul>
<p>dbt に入門しようとしている人にとって、dbt に関するイメージを具体的にする助けとなれば幸いです！</p>
]]></content>
    <summary type="html">データ分析基盤の構築や運用において注目を集めているdbt の入門記事です。dbtを活用して、データ変換の一連の手順を示すことで、これからdbtを試してみようと考えている方の導入を支援できれば幸いです。。</summary>
    <category term="DataEngineering" scheme="https://future-architect.github.io/categories/DataEngineering/"/>
    <category term="BigQuery" scheme="https://future-architect.github.io/tags/BigQuery/"/>
    <category term="GoogleCloud" scheme="https://future-architect.github.io/tags/GoogleCloud/"/>
    <category term="dbt" scheme="https://future-architect.github.io/tags/dbt/"/>
  </entry>
  <entry>
    <title>はじめてのStreamlit with Google Cloud</title>
    <link href="https://future-architect.github.io/articles/20250422a/"/>
    <id>https://future-architect.github.io/articles/20250422a/</id>
    <published>2025-04-21T15:00:00.000Z</published>
    <updated>2025-04-21T15:00:00.000Z</updated>
    <author><name>柴田健太</name></author>
    <content type="html"><![CDATA[<img fetchpriority="high" src="/images/2025/20250422a/top.png" alt="" width="800" height="502">

<p>春の入門祭り2025 7日目です。こんにちは、製造・エネルギーサービス事業部の柴田です。<br>日本の製造業・エネルギー業をDXの力で元気にすることを目指しています。</p>
<p>企業でのDX推進においてPythonは多くの場面で活躍の場面があり、習得スキルとしても人気があります。</p>
<p>Streamlitは、Pythonを使って簡単にインタラクティブなWebアプリケーションとして共有できるライブラリで多くの採用実績があり、Snowflakeが買収したため今後の発展も期待できます。本記事では、Google CloudのVertex AI Workbenchを活用してStreamlitアプリを開発し、Google Cloud Runにデプロイするまでの手順を詳しく解説します。ローカル環境のセットアップは不要です。</p>
<h2 id="前提条件">前提条件</h2><ul>
<li>Google Cloudアカウントを用意していること</li>
<li>Google Cloud プロジェクトが作成済みであること</li>
</ul>
<h2 id="全体の流れ">全体の流れ</h2><ul>
<li>Webアプリを作成する</li>
<li>DockerイメージをArtifact Registryにプッシュする</li>
<li>Artifact RegistryにプッシュしたDockerイメージをCloud Runにデプロイする</li>
</ul>
<h2 id="Webアプリの作成">Webアプリの作成</h2><h3 id="Google-Cloud-Notebooks-インスタンスの作成">Google Cloud Notebooks インスタンスの作成</h3><p>まずはStreamlitアプリを開発するためのGoogle Cloud Notebooksインスタンスを作成します。</p>
<ol>
<li><p>Vertex AI Workbench に移動<br>Google Cloud Consoleのナビゲーションメニューから「Vertex AI」&gt;「Workbench」に移動します。見つからない場合は検索ウィンドウでVertex AIを検索して直接移動してくださいね</p>
</li>
<li><p>新しいノートブックの作成<br>Viewで「インスタンス」を指定し、「新規作成」をクリックし、適切な環境を選択します。</p>
</li>
<li><p>ノートブックを開く<br>インスタンスが作成され、準備が完了したら、「JupyterLab を開く」をクリックします。</p>
</li>
</ol>
<h3 id="Streamlit-アプリの開発">Streamlit アプリの開発</h3><p>JupyterLabが開いたら、新しいPythonファイルを作成し、Streamlitアプリのコードを記述します。</p>
<ol>
<li><p>新しい Python ファイルの作成<br>JupyterLabのランチャー（または「ファイル」&gt;「新規」&gt;「Python 3」）を開き、新しいIPython Notebookを作成します</p>
</li>
<li><p>Streamlit とその他のライブラリのインストール<br>最初のセルに以下のコードを入力し、実行してStreamlitをインストールします</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">!pip install streamlit</span><br></pre></td></tr></table></figure>
</li>
<li><p>Streamlit アプリのコード記述<br>新しいセルを作成し、Streamlitアプリのコードを記述します。ローカル環境の例と同じコードを使用できます。</p>
<figure class="highlight py"><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> streamlit <span class="keyword">as</span> st</span><br><span class="line"></span><br><span class="line">st.title(<span class="string">&quot;My First Streamlit App on Google Cloud Notebooks&quot;</span>)</span><br><span class="line">st.write(<span class="string">&quot;Hello from Streamlit running on Google Cloud Notebooks!&quot;</span>)</span><br><span class="line"></span><br><span class="line">name = st.text_input(<span class="string">&quot;Enter your name&quot;</span>, <span class="string">&quot;World&quot;</span>)</span><br><span class="line">st.write(<span class="string">f&quot;Hello, <span class="subst">&#123;name&#125;</span>!&quot;</span>)</span><br></pre></td></tr></table></figure>
</li>
<li><p>Python ファイルとして保存<br>  「ファイル」&gt;「名前を付けて保存」を選択し、ファイル名を <code>app.py</code> として保存します。</p>
</li>
</ol>
<h2 id="Dockerイメージをプッシュ">Dockerイメージをプッシュ</h2><h3 id="Dockerfile-の作成">Dockerfile の作成</h3><p>Google Cloud Runにデプロイするために、Dockerコンテナを作成します。JupyterLabのファイルブラウザで、<code>app.py</code> と同じディレクトリに <code>Dockerfile</code> という名前の新しいテキストファイルを作成し、以下の内容を記述します。</p>
<figure class="highlight dockerfile"><table><tr><td class="code"><pre><span class="line"><span class="keyword">FROM</span> python:<span class="number">3.9</span>-slim-buster</span><br><span class="line"></span><br><span class="line"><span class="keyword">WORKDIR</span><span class="language-bash"> /app</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">COPY</span><span class="language-bash"> requirements.txt .</span></span><br><span class="line"><span class="keyword">RUN</span><span class="language-bash"> pip install -r requirements.txt</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">COPY</span><span class="language-bash"> . .</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># streamlitのデフォルトPORT番号</span></span><br><span class="line"><span class="keyword">EXPOSE</span> <span class="number">8501</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">HEALTHCHECK</span><span class="language-bash"> CMD curl --fail http://localhost:8501/_stcore/health</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">ENTRYPOINT</span><span class="language-bash"> [<span class="string">&quot;streamlit&quot;</span>, <span class="string">&quot;run&quot;</span>, <span class="string">&quot;app.py&quot;</span>, <span class="string">&quot;--server.enableCORS=false&quot;</span>, <span class="string">&quot;--server.port=8501&quot;</span>]</span></span><br></pre></td></tr></table></figure>

<h4 id="requirements-txt-の作成">requirements.txt の作成</h4><p><code>app.py</code> と同じディレクトリに <code>requirements.txt</code> という名前の新しいテキストファイルを作成し、アプリに必要なPythonパッケージを記述します。今回はstreamlit以外を使用していないため以下でOKです。</p>
<figure class="highlight text"><table><tr><td class="code"><pre><span class="line">streamlit</span><br></pre></td></tr></table></figure>

<h3 id="Docker-イメージのビルドと-Artifact-Registry-へのプッシュ">Docker イメージのビルドと Artifact Registry へのプッシュ</h3><p>Cloud Notebooksインスタンス内でDockerイメージをビルドし、Artifact Registryにプッシュします。</p>
<h4 id="Artifact-Registry-API-の有効化">Artifact Registry API の有効化</h4><p>まだ有効にしていない場合は、Google Cloud Console で Artifact Registry API を有効にします。</p>
<h4 id="Artifact-Registry-リポジトリの作成">Artifact Registry リポジトリの作成</h4><p>Google Cloud Console または <code>gcloud</code> コマンドを使用して、Docker イメージを保存する Artifact Registry リポジトリを作成します。リージョン（例: <code>asia-northeast1</code>）とリポジトリ形式（<code>Docker</code>）を指定します。</p>
<figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">gcloud artifacts repositories create &lt;repository-name&gt; \</span><br><span class="line">  --repository-format docker \</span><br><span class="line">  --location &lt;your-gcp-region&gt; \</span><br></pre></td></tr></table></figure>

<figure class="highlight sh"><figcaption><span>コマンド例</span></figcaption><table><tr><td class="code"><pre><span class="line">gcloud artifacts repositories create test-repository \</span><br><span class="line">  --repository-format docker \</span><br><span class="line">  --location asia-northeast1</span><br></pre></td></tr></table></figure>

<p>このようにArtifact RegistryにDocker形式のリポジトリができていればOKです<br><img src="/images/2025/20250422a/image.png" alt="" width="1200" height="69" loading="lazy"></p>
<h5 id="Docker-イメージのビルド">Docker イメージのビルド</h5><p>ターミナルで、<code>Dockerfile</code> があるディレクトリに移動し、以下のコマンドを実行して Docker イメージをビルドします。<code>&lt;your-gcp-region&gt;</code> と <code>&lt;repository-name&gt;</code>、<code>&lt;image-name&gt;</code> は適切に置き換えてください。</p>
<figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">docker build -t &lt;your-gcp-region&gt;-docker.pkg.dev/&lt;your-gcp-project-id&gt;/&lt;repository-name&gt;/&lt;image-name&gt; .</span><br></pre></td></tr></table></figure>

<figure class="highlight sh"><figcaption><span>コマンド例</span></figcaption><table><tr><td class="code"><pre><span class="line">docker build -t asia-northeast1-docker.pkg.dev/sample-project/test-repository/myapp .</span><br></pre></td></tr></table></figure>

<h4 id="Docker-に-Artifact-Registry-の認証を設定">Docker に Artifact Registry の認証を設定</h4><p>以下のコマンドを実行して、Docker が Artifact Registry にプッシュできるように認証します。</p>
<figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">gcloud auth configure-docker &lt;hostname-list&gt;</span><br></pre></td></tr></table></figure>

<p><code>hostname-list</code>は、認証ヘルパー構成に追加するリポジトリ ホスト名のカンマ区切りのリストで、Artifact Registryのホスト名はロケーションと紐付いているため、<code>&lt;location&gt;-docker.pkg.dev</code>という形になります。</p>
<figure class="highlight sh"><figcaption><span>コマンド例</span></figcaption><table><tr><td class="code"><pre><span class="line">gcloud auth configure-docker asia-northeast1-docker.pkg.dev</span><br></pre></td></tr></table></figure>

<h4 id="Docker-イメージのプッシュ">Docker イメージのプッシュ</h4><p>ビルドした Docker イメージを Artifact Registry にプッシュします。先ほどビルドした際のタグを使用します。</p>
<figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">docker push &lt;your-gcp-region&gt;-docker.pkg.dev/&lt;your-gcp-project-id&gt;/&lt;repository-name&gt;/&lt;image-name&gt;</span><br></pre></td></tr></table></figure>

<figure class="highlight sh"><figcaption><span>コマンド例</span></figcaption><table><tr><td class="code"><pre><span class="line">docker push asia-northeast1-docker.pkg.dev/grassroot-ck/test-repository/myapp</span><br></pre></td></tr></table></figure>

<p>実行すると、このようにリポジトリ内にイメージがプッシュされます。</p>
<img src="/images/2025/20250422a/image_2.png" alt="" width="533" height="318" loading="lazy">

<h2 id="Google-Cloud-Run-へのデプロイ">Google Cloud Run へのデプロイ</h2><p>プッシュしたDockerイメージをGoogle Cloud Runにデプロイします。</p>
<h3 id="Cloud-Run-へのデプロイコマンド実行">Cloud Run へのデプロイコマンド実行</h3><p>ターミナルで以下のコマンドを実行します。<code>&lt;your-gcp-project-id&gt;</code> と <code>&lt;your-gcp-region&gt;</code> はご自身のGCPプロジェクトIDとリージョンに置き換えてください（例: <code>asia-northeast1</code>）</p>
<figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">gcloud run deploy &lt;app-name&gt; \</span><br><span class="line">--image gcr.io/&lt;your-gcp-project-id&gt;/&lt;image-name&gt; \</span><br><span class="line">--platform managed \</span><br><span class="line">--region &lt;your-gcp-region&gt; \</span><br><span class="line">--port 8501 \</span><br><span class="line">--allow-unauthenticated</span><br></pre></td></tr></table></figure>

<p>&lt;コマンドの説明&gt;</p>
<ul>
<li><code>gcloud run deploy &lt;app-name&gt;</code>: Cloud Runサービスの名前を指定します</li>
<li><code>--image gcr.io/&lt;your-gcp-project-id&gt;/&lt;image-name&gt;</code>: デプロイするDockerイメージを指定します</li>
<li><code>--platform managed</code>: フルマネージド環境にデプロイします</li>
<li><code>--region &lt;your-gcp-region&gt;</code>: デプロイするリージョンを指定します</li>
<li><code>--port 8501</code>: コンテナがリッスンするポートを指定します（Streamlitのデフォルトポート）</li>
<li><code>--allow-unauthenticated</code>: 認証なしでアクセスできるようにします（必要に応じて変更してください）</li>
</ul>
<figure class="highlight sh"><figcaption><span>コマンド例</span></figcaption><table><tr><td class="code"><pre><span class="line">gcloud run deploy myapp \</span><br><span class="line">--image gcr.io/sample-project/myapp \</span><br><span class="line">--platform managed \</span><br><span class="line">--region asia-northeast1 \</span><br><span class="line">--port 8501 \</span><br><span class="line">--allow-unauthenticated</span><br></pre></td></tr></table></figure>

<p>Cloud Runにデプロイされました。</p>
<img src="/images/2025/20250422a/image_3.png" alt="" width="1200" height="77" loading="lazy">

<h3 id="デプロイの確認">デプロイの確認</h3><p>デプロイが完了すると、Cloud RunサービスへのURLが表示されます。このURLをブラウザで開くと、デプロイしたStreamlitアプリにアクセスできます。もしアクセスできなければセキュリティタブで、未認証の呼び出しを許可するように設定してください。</p>
<img src="/images/2025/20250422a/image_4.png" alt="" width="993" height="508" loading="lazy">

<p>このようなアノテーションツールも作成できます。Cloud上で動かすことでツールやデータを一括管理できるため、一歩進んだPythonの業務利用ができます。このツールではPlotlyと組み合わせて、ラベルを入力する度にインタラクティブに左下のグラフが更新されるようにしています。※Plotlyについては参考資料の記事を参考にしてください。</p>
<img src="/images/2025/20250422a/sample_tool.avif" width="1400" height="927" loading="lazy">

<h2 id="まとめ">まとめ</h2><p>Google Cloud Notebooksを利用することで、ローカル環境をセットアップすることなく、GCP上でStreamlitアプリを開発からデプロイまで一貫して行うことができます。Cloud Runのサーバーレスな環境により、Streamlitアプリを簡単に公開し、共有できます。</p>
<p>もし、機密情報を用いるなどアクセス制限が必要な場合には追加の対応が必要ですので、注意してください。</p>
<p>この手順を参考に、ぜひGoogle Cloud Notebooksを活用してStreamlitアプリの開発とデプロイを試してみてください。</p>
<h2 id="参考">参考</h2><ul>
<li>Artifact Registry for Docker への認証を構成する</li>
<li>イメージを push および pull する</li>
<li>Plotly.pyによるデータ可視化のすすめ</li>
</ul>
]]></content>
    <summary type="html">Streamlitは、Pythonを使って簡単にインタラクティブなWebアプリケーションとして共有できるライブラリで多くの採用実績があり、Snowflakeが買収したため今後の発展も期待できます。本記事では、Google CloudのVertex AI Workbenchを活用してStreamlitアプリを開発し、Google Cloud Runにデプロイするまでの手順を詳しく解説します。</summary>
    <category term="DataEngineering" scheme="https://future-architect.github.io/categories/DataEngineering/"/>
    <category term="CloudRun" scheme="https://future-architect.github.io/tags/CloudRun/"/>
    <category term="GoogleCloud" scheme="https://future-architect.github.io/tags/GoogleCloud/"/>
    <category term="Python" scheme="https://future-architect.github.io/tags/Python/"/>
    <category term="可視化" scheme="https://future-architect.github.io/tags/%E5%8F%AF%E8%A6%96%E5%8C%96/"/>
  </entry>
  <entry>
    <title>区分値設計 再考</title>
    <link href="https://future-architect.github.io/articles/20250321a/"/>
    <id>https://future-architect.github.io/articles/20250321a/</id>
    <published>2025-03-20T15:00:00.000Z</published>
    <updated>2025-03-20T15:00:00.000Z</updated>
    <author><name>武田大輝</name></author>
    <content type="html"><![CDATA[<h2 id="はじめに">はじめに</h2><p>システムにおける「区分」とはカテゴライズ可能な値の集合体を表すものであり、「区分値」とはその集合に属する個々の識別子を指します。</p>
<p>たとえば、あるアイテムの「ステータス」という区分には「下書き」「レビュー中」「承認済み」といった区分値が含まれます。</p>
<p>一見すると、こうした区分値は単なるラベルでしかなく、設計の検討余地があまりないように思えるかもしれません。しかし実際には、区分値の扱い方次第で、システムの柔軟性・拡張性・保守性などに大きな差が生まれます。</p>
<p>本記事では、この「区分値」の設計について、あらためて整理し、より優れたアプローチを再考していきます。</p>
<h2 id="本記事の前提">本記事の前提</h2><p>区分値の設計について語る前に、設計の前提となるシステムについて整理しておきます。</p>
<p>本記事では、フロントエンドとバックエンドが分離され、Web API を通じてやりとりする Web アプリケーション（いわゆるモダン Web アプリケーションと言われるもの）を前提とします。さらにフロントエンドとバックエンドの双方が自組織の管理下にあるような状況を想定しています。</p>
<p>このようなシステムにおいて、区分はいろいろなところで登場します。</p>
<p>冒頭で例に挙げた「ステータス」という区分を持つ場合、データベースにはアイテムの属性としてステータスを保持するでしょう。エンドユーザは画面からセレクトボックスなどで検索対象のステータスを選択し、そのステータスは検索条件として Web API のリクエストパラメータで渡されます。Web API サーバはそのステータスを元にデータベースから対象のアイテムだけを選択し結果を返却する、と言った具合です。</p>
<img fetchpriority="high" src="/images/2025/20250321a/overview.drawio.png" alt="overview.drawio.png" width="821" height="221">

<h2 id="設計上の論点">設計上の論点</h2><p>前置きが長くなりましたが、ここからは区分値の設計について、具体的な論点をもとに説明していきます。</p>
<h3 id="区分値の表現（Symbolic-Value-vs-Semantic-Value）">区分値の表現（Symbolic Value vs Semantic Value）</h3><p>まず最初に、区分値を記号としての数値（Symbolic Value：シンボル値）として表現するか、意味のある文字列（Semantic Value：セマンティック値）として表現するかという設計判断が存在します。たとえば「ステータス」区分を取り上げると、シンボル値では <code>01</code>（下書き）・<code>02</code>（レビュー中）・<code>03</code>（承認済み）となり、セマンティック値では <code>draft</code>・<code>in_review</code>・<code>approved</code> となります。</p>
<p>シンボル値による表現は、伝統的な業務システムにおいて長年にわたって広く使われてきた手法ですが、結論から言うと昨今のモダンな開発現場においては <strong>「セマンティック値」での表現を第一に検討することが望ましい</strong> と考えます。</p>
<h4 id="2-つの表現の比較">2 つの表現の比較</h4><p>いくつかの観点でこの 2 つの表現を比較してみましょう。</p>
<img src="/images/2025/20250321a/classification_representation.drawio.png" alt="classification_representation.drawio.png" width="820" height="280" loading="lazy">

<h5 id="ヒューマンリーダビリティ">ヒューマンリーダビリティ</h5><p>セマンティック値はその文字列自体が意味を持つため、値を見ただけで直感的に理解しやすいです。シンボル値はその数字を見ただけでは意味が分かりにくく、別途コード表（区分値とその意味の対応表）を参照するなどして意味を確認する必要があります。<br>セマンティック値は Web API の利用者や運用保守者にとっても多くの利点があります。たとえば Web API においては、リクエストやレスポンスで意味のある値を直接やりとりできるため、利用者は実装やドキュメントを確認せずとも内容を把握しやすくなります。さらに、ログやデータベース上でも区分値の意味がそのまま伝わるため、運用時の調査や障害対応においても確認がスムーズです。</p>
<h5 id="変更容易性">変更容易性</h5><p>区分値の追加や削除のしやすさを指します。たとえば、先ほどのステータス区分に「レビュー待ち」を追加したくなったケースを考えます。セマンティック値で表現している場合は<code>ready_for_review</code>を新たな区分値として追加するだけで済みます。</p>
<p>では、シンボル値の場合はどうでしょうか。単純に <code>04</code> を追加すると「<code>01</code>（下書き）」「<code>02</code>（レビュー中）」「<code>03</code>（承認済み）」「<code>04</code>（レビュー待ち）」となり、この順序に違和感を覚える人がいるかもしれません。おそらく多くの人は、「<code>01</code>（下書き）」「<code>02</code>（レビュー待ち）」「<code>03</code>（レビュー中）」「<code>04</code>（承認済み）」のように、ステータスの遷移の順序に沿った並びを期待するでしょう。</p>
<p>このように本来は意味を持たないはずのシンボル値が、暗黙的に「順番」という意味をもってしまうというジレンマが生じます。これに対応しようとすると、シンボル値の先頭桁に意味を持たせてグルーピングする設計（例. <code>00</code> ~ <code>09</code> は草稿・編集系、<code>10</code> ~ <code>19</code> はレビュー系、<code>20</code> ~ <code>29</code> は承認・公開系など）が検討されることもあります。一見すると拡張性があるように見えますが、この設計は結局のところシンボル値に意味を持たせているにすぎません。セマンティック値に比べて直感的ではなく、むしろわかりづらい意味付けをシンボル値の桁構成に押し込めているだけとも言えるでしょう。</p>
<p>一方で、シンボル値にも一定のメリットがあります。たとえば、当初は「<code>01</code>（下書き）」「<code>02</code>（レビュー中）」「<code>03</code>（承認済み）」という区分だったとしても、あとからより適切な概念（例：<code>01</code>（記事構成案）、<code>02</code>（デスクチェック）、<code>03</code>（掲載許可）など）がプロジェクトの進行に伴って「発見」されるケースがあります。こうした場合、シンボル値であれば論理名（ラベル）だけを変更すれば済み、システム的な影響範囲は小さく抑えられます。</p>
<p>セマンティック値では <code>draft</code> を <code>article_composition_proposal</code> に変更したいとなった場合、物理名の変更が発生し、コード修正や連携先への影響も大きくなる恐れがあります。</p>
<h5 id="命名コスト">命名コスト</h5><p>命名コストとは、適切な区分値の名前を決めるための時間・労力・議論のコストを指します。</p>
<p>セマンティック値の場合、命名は慎重に行わなければなりません。例として挙げたステータスのようなわかりやすい区分であれば大したコストはかかりませんが、日本のエンタープライズなシステムや業務専門性の高いシステムには、わかりやすい名前を付けることがそもそも難しい区分値（例. 英語での表現が難しい <sup id="fnref:1">1</sup> 専門用語や概念など）が一定存在します。</p>
<p>このような場合、適切な命名を検討し、チーム内で合意形成をするコストは非常に大きくなる可能性があります。</p>
<p>ただし、シンボル値の場合でも、内部のプログラムとしてはその値に対応する列挙体なり定数なりを定義するはずですので、何かしら命名するという行為とそこにかかるコストは変わらないのかもしれません。</p>
<h5 id="パフォーマンス">パフォーマンス</h5><p>パフォーマンスについては、ストレージ・通信効率の観点とクエリ性能の観点の 2 つがあります。</p>
<p>ストレージ・通信効率の観点では、シンボル値（通常 2~3 桁の数字）はセマンティック値と比べてデータ量が小さいため、ストレージや通信の負荷が低くなります。クエリ性能の観点ではリレーショナルデータベースのインデックスを利用したクエリにおいて、固定長数値のシンボル値の方が可変長文字列のセマンティック値よりも高速に動作することが期待できます。</p>
<p>ただし、この性能差を意識しなければならないほどの性能要件があるシステムはほとんどないでしょう。一昔前のシステムではストレージやメモリが限られていたためデータ量を減らすことが最優先でしたが、クラウド環境や現代のハードウェア性能の向上により、この制約を意識することはほぼなくなりました。また文字列検索の最適化をはじめ、データベースの進化によりセマンティックを採用しても、現実的なデータ量であればクエリ性能に大きな影響を与えないケースが大半です。</p>
<p>結論として、パフォーマンスの差は理論上存在するものの、ほとんどのシステムでは影響が軽微であり、シンボル値とセマンティック値の良し悪しを判断する上で重要な要素にはなりません。</p>
<h4 id="結論どうすべきか">結論どうすべきか</h4><p>先に記載した通り、Web API を通じたデータのやりとりが主流となっている現代の Web アプリケーションシステムにおいては <strong>「セマンティック値」による区分値の表現を第一に検討すべき</strong> だと考えます。</p>
<p>セマンティック値は、開発者、運用保守者の双方に直感的で扱いやすく、可読性・拡張性といった点でシンボル値よりも優れています。<br>もちろん、命名コストやパフォーマンスといった観点でシンボル値に一定の利点があるのも事実ですが、それらは実際の開発・運用において致命的な差となることはほとんどありません。むしろ、意味を隠蔽したシンボル値を維持することのデメリットの方が、長期的な保守性やチーム内の共通理解の観点から大きな負債となる可能性があります。</p>
<p>もしくは、より可読性の求められる外部向けの表現（Web API のリクエスト・レスポンス）はセマンティック値を利用し、内部向けの表現（データベース）はシンボル値を利用するというハイブリッド設計も考えられるかもしれません。ただし、セマンティック値とシンボル値の管理コストやマッピング（変換）コストを考慮すると、あえて使い分けるメリットはないと考えます。</p>
<p>ただし私も作成に関わった PostgreSQL 設計ガイドライン では、DB としてシンボル値での表現を推奨する形にしており、このあたりはまだまだ議論がありそうです。</p>
<h4 id="シンボル値を使うべきケース">シンボル値を使うべきケース</h4><p>セマンティックな値を定義することによるメリットが極端に少ない、もしくはデメリットが極端に大きいケースが一定存在するのも事実であり、そのような場合はシンボル値を検討すべきです。具体的には次のようなケースが考えられます。</p>
<ul>
<li>ビジネスドメインが日本ローカルに特化（例. 官公庁系システムなど）しており、業界特有の専門用語が日本語ベースでしか存在しない（英語で表現しづらい、表現することで返って可読性が低下する恐れのある）場合</li>
<li>その現場や周辺のシステムにおいて、シンボル値での区分表現が慣例となっており、セマンティック値を用いることで返って混乱を招く恐れがある場合</li>
<li>依存するシステムがシンボル値を利用しており、その値を内部のセマンティック値に変換するコストが大きいなど、シンボル値とセマンティック値の混在が避けられない場合</li>
</ul>
<p>また、少々悩ましいのが ISO 5218 で定められている性別区分など、国際規格としてシンボル値が定められているケースです。シンボル値の利用がデファクトであると考えられるケースにおいてはシンボル値を利用するという判断が考えられます。</p>
<p>どちらの表現を利用するにせよ、これまでの慣習的な設計にとらわれず（言い換えれば思考停止で設計せず）自分たちのシステムにとってより望ましい表現を再検討することは大きな意味を持つと考えます。</p>
<p>いずれにしてもこのような区分を含むデータを、データ基盤などに集約してAIに分析させる、といった未来を予想すると、各システムごとに最適化するよりは、全社的なデータモデルをどうするかが重要であり、シンボル値とセマンティック値のどちらを採用するにせよ組織内で統一することが重要なのかもしれません。</p>
<h3 id="区分値の管理">区分値の管理</h3><p>区分値自体は Enum なり定数なりで定義する形になりますが、この区分値をクライアント側とサーバ側のどちらで管理するかという設計判断が存在します。</p>
<p>この話はあまりピンとこない方が多いかもしれません。区分値は一覧表示やロジック（例. 条件分岐）などさまざまな用途で使われ、その用途によって考え方が変わるため、一概にどちらとは言えないのですが順を追って説明していきます。</p>
<p>まずは一番単純な例として、ユーザが Web 画面上からセレクトボックスでステータス区分を選択し、その選択した値を Web API のリクエストパラメータとして送信するケースを考えてみましょう。このとき、純粋な区分値の一覧だけではなく、区分値に付随する情報として区分値の表示名や表示順の定義が必要となります。これらの情報の管理には大きく 2 つのアプローチが考えられます。</p>
<ul>
<li><strong>クライアント側で管理</strong><br>区分値、表示名、表示順の情報をクライアント側に持たせる方式です。<br>この方式では、クライアント内に定義された情報をもとに、Web API を呼び出すことなく区分の一覧を取得し、セレクトボックスなどの UI を描画できます。</li>
<li><strong>サーバ側で管理</strong><br>区分値、表示名、表示順の情報をサーバ側に持たせる方式です。<br>この方式では、クライアントは初回表示時などに Web API をコールして区分値の一覧を取得し、それをもとにセレクトボックスなどの UI を描画します。</li>
</ul>
<p>結論だけ先に言うと、区分値はどちらか一方のみで管理できる代物ではないため、<strong>責務を意識してそれぞれで分離管理することが望ましい</strong>と考えます。</p>
<h4 id="２つの方式の比較">２つの方式の比較</h4><p>いくつかの観点でこの 2 つの方式を比較してみましょう。</p>
<img src="/images/2025/20250321a/classification_management.drawio.png" alt="classification_management.drawio.png" width="821" height="381" loading="lazy">

<h5 id="責務の分離">責務の分離</h5><p>区分値の表示名や表示順といった情報は基本的に UI のみに影響する情報であり、責務の分離という観点ではクライアント側で管理することが望ましいと考えます。</p>
<p>複数のクライアントアプリケーション（Web アプリケーション、モバイルアプリケーションなど）が存在する場合、クライアントごとに表示名や文言表現を変えたいというニーズが発生することもあります。このようなケースでは、サーバ側で一元的に表示名を管理してしまうとクライアントごとの個別表現が難しくなるため、クライアント側で表示情報を持つことでより柔軟な UI 制御が可能になります。</p>
<h5 id="構成のシンプルさ">構成のシンプルさ</h5><p>クライアント側で区分値を管理する場合、画面描画のために区分値取得の Web API を呼び出す必要がありません。非同期処理や取得失敗時の考慮が不要になるため、実装がシンプルになり、画面の描画も高速です。</p>
<p>一方、サーバ側で区分値を管理する場合は、初回表示時に Web API の呼び出しが必要となり、ローディング制御やフォールバック処理など複雑さが大きくなります。待機時間によっては UX が低下する恐れもあるため、キャッシュの活用やプリフェッチの導入、スケルトン UI の表示など、UX への配慮が求められます。</p>
<h5 id="変更時の柔軟性">変更時の柔軟性</h5><p>区分値の追加・削除・変更において、クライアントサイド管理の場合はクライアントアプリケーション、サーバサイド管理の場合サーバアプリケーションのデプロイが必要になります。</p>
<p>この 2 つは大きな差がないように思えるかもしれませんが、次のようなケースにおいてはサーバ側で管理することによる効果が大きくなります。</p>
<ul>
<li>異なるクライアントアプリケーションが複数あり、サーバ側で区分値を中央集権的にコントロールすることで運用・保守コストが小さくなる場合</li>
<li>クライアントアプリケーションが Web アプリケーションではなくモバイルアプリケーションなどデプロイにかかるコスト（審査コストなど）が大きい場合</li>
</ul>
<h5 id="利用者による制御">利用者による制御</h5><p>クライアント側で管理している場合、表示内容の調整には開発者によるコードの修正が必要になります。</p>
<p>アプリケーションの管理権限を持った利用者などが、区分値の表示名や表示順を直接コントロールしたい場合は、区分値をサーバ側（データベース）で管理し、管理画面などから編集できるようにしておくのが適しています。</p>
<h4 id="実際は双方での管理が必要">実際は双方での管理が必要</h4><p>ここまではクライアントサイド管理とサーバサイド管理の違いをわかりやすく伝えるため、区分値の一覧表示というシンプルなケースを取り上げて説明しましたが、実際のところ話はもう少し複雑です。なぜなら区分値は一覧表示だけではなく、ロジックとしての制御にも使用されるからです。</p>
<p>たとえば、クライアント側で区分値を管理した場合でも、その区分値が Web API のリクエストパラメータでサーバに投げられたとき、サーバ側はその区分値が受け入れ可能な値かどうかをチェックするでしょう。場合によっては、区分値の値に応じて条件分岐などが必要になるかもしれません。</p>
<p>このような場合は、サーバ側はロジックを制御するために区分値の列挙体や定数を定義することになります。</p>
<figure class="highlight ts"><table><tr><td class="code"><pre><span class="line"><span class="comment">// 区分値の定義（列挙体や定数）が必要</span></span><br><span class="line"><span class="keyword">if</span> (req.<span class="property">status</span> != <span class="title class_">Status</span>.<span class="property">Draft</span>) &#123;</span><br><span class="line">  ...</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>その逆も然り、サーバ側で区分値を管理した場合でも、クライアント側において画面で選択された区分値の値に応じて条件分岐をする場合などは、クライアント側で区分値に対するアクセッサの定義（サーバから取得した区分値一覧の中から対象の区分値を取得するためのキー定義など）が必要になります。</p>
<figure class="highlight ts"><table><tr><td class="code"><pre><span class="line"><span class="comment">// サーバから取得した区分値の一覧から特定の区分値を取得するための定義が必要</span></span><br><span class="line"><span class="keyword">if</span> (input.<span class="property">value</span> != data.<span class="property">status</span>[<span class="variable constant_">STATUS_KEYS</span>.<span class="property">DRAFT</span>]) &#123;</span><br><span class="line">  ...</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>このように、実際のところはどちらか一方で区分値を管理すれば十分、という単純な話ではなく、クライアントとサーバの双方において何らかの形で区分値にアクセス・解釈するケースがほとんどです。そのため、区分値の定義方式はクライアント側とサーバ側のそれぞれで管理する <strong>「分離管理型」</strong> とサーバ側で極力一元的に管理する <strong>「サーバ中心管理型」</strong> の 2 つに分類して考えることができます。</p>
<img src="/images/2025/20250321a/classification_management_actual.drawio.png" alt="classification_management_actual.drawio.png" width="821" height="421" loading="lazy">

<p>冒頭述べたとおり、基本的には<strong>責務を意識して必要な情報をそれぞれ管理することが重要</strong>であり、クライアントサイドは区分値の一覧および表示名や表示順を管理、サーバサイドはロジック制御のため区分値の一覧を管理する分離管理方式が望ましいと考えます。</p>
<p>サーバサイドで「一元的にコントロールしたいケース」や「管理権限をもった利用者がコントロールしたいケース」においてのみ、区分値取得の Web API 化するサーバ主体の管理方式を検討すべきです。</p>
<p>ちなみに筆者の観測範囲に限った話ですが、従来のサーバサイドレンダリング &#x2F; MPA 時代の設計になごりのある組織においては、暗黙的にサーバ中心管理となるケースが多いと感じています。</p>
<p>これは、かつてはサーバ側が UI 表示も含めてすべてを制御していたという背景があることに起因していそうです。SPA への移行後も、既存のロジックやデータ構造を維持する流れで、サーバ側で区分値を一元管理する設計思想が引き継がれているのではないかと考えます。</p>
<h3 id="区分値の定義場所">区分値の定義場所</h3><p>サーバ側で区分値を定義する場合、ソースコードの列挙体や定数などで区分値を定義するか、データベースのマスタ（例. 区分値マスタ）で区分値を定義するかという設計判断が存在します。</p>
<p>結論から言うとこれは <strong>ソースコードで定義する方式</strong> が望ましいと考えます。従来 PL&#x2F;SQL などでデータベース内にビジネスロジックが実装されていた時代は、区分値をデータベースで保持する形が合理的でしたが、現代のモダンなシステムにおいて、もはやその意味はなくなりました。IDE や型システムによる保管や検出、テスト容易性やデプロイ容易性などさまざまな観点からソースコードで定義するメリットが大きいと考えられます。</p>
<h3 id="フラグの取り扱い">フラグの取り扱い</h3><p>フラグとは、ある状態や条件を 2 値（true&#x2F;false）で表すためのものであり、言い換えれば「2 値のみを持つ区分の一種」と言えます。<br>このフラグに関しては、ほかの区分値にはない設計上の論点がいくつかあります。</p>
<p>詳細な話をする前に、前提として、対象のフラグが本当に 2 値のみで完結するのかどうかを見極めることが重要 <sup id="fnref:2">2</sup> です。たとえば「有効フラグ」というものが必要になったとき、それは「有効」「無効」の 2 値を表すフラグではなく「有効」「無効」「一時停止」「廃止」など、将来的に選択肢が増える <strong>状態</strong> であるかもしれないと疑ってください。このようなものは初めからフラグではなく区分として取り扱うことが適切です。</p>
<h4 id="フラグ値の表現">フラグ値の表現</h4><p>区分値の表現 ではシンボル値とセマンティック値の表現パターンが存在することを説明しましたが、フラグ値の表現は一般的に「0&#x2F;1 による表現」と「真偽値（true&#x2F;false）による表現」が考えられます。前者がシンボル値、後者がセマンティック値である<sup id="fnref:3">3</sup>ととらえることもできます。</p>
<p>区分値の表現と同様、より意味が明確な「真偽値（true&#x2F;false）による表現」を推奨します。Web API の外部表現においては真偽値の使用が標準的になっており、内部ロジックの記述性としても真偽値を用いる方がシンプルでわかりやすいと考えます。</p>
<img src="/images/2025/20250321a/flag_representation.drawio.png" alt="flag_representation.drawio.png" width="820" height="280" loading="lazy">

<p>ただし、区分値の表現としてシンボル値による表現を選択した場合は、フラグ値の表現も 0&#x2F;1 による表現とする方が、設計が統一されるため望ましいと考えます。</p>
<h4 id="フラグ値の個別定義">フラグ値の個別定義</h4><p>2 値で表現されるフラグが複数あったときに、各フラグごとに「ON」「OFF」や「有効」「無効」といった値を列挙体や定数で個別定義するのかという論点があります。</p>
<p>個別定義することで、特定のフラグ値の利用箇所がソースコード上検出し易くなったり、区分と同様に各フラグ値に紐づけて表示名や表示順を管理できたりと運用・保守上の効果が期待できます。</p>
<p>ただし、フラグ値を真偽値（true&#x2F;false）で表現する場合は、ロジックの記述が冗長になる（例. <code>if(isActive == consts.Active)</code> のような真偽値どうしの比較になる）可能性があり、この場合まずは個別定義せず実装をシンプルにするところを意識して始めてみるのがよいのではないかと考えます。</p>
<h2 id="おわりに">おわりに</h2><p>本記事では、区分値という一見些細に思えるしくみについて、あらためて設計観点を掘り下げてきました。</p>
<p>区分値の表現方法、管理場所、定義場所、さらにはフラグ値の扱い方に至るまで、多くの選択肢が存在します。そしてそれぞれの選択には、必ずトレードオフがあり、すべての場面で正解となる万能な方式は存在しません。</p>
<p>大切なのは「今までそうしていたから」という慣習に従うのではなく、自分たちのシステムにおいてどのような設計がより適しているのか、今一度考え直してみるきっかけになれば幸いです。</p>
<h2 id="参考">参考</h2><ul>
<li>DB で区分値などの CODE は数値ではなく文字列を利用するべき</li>
<li>SQL アンチパターン幻の第 26 章「とりあえず削除フラグ」</li>
<li>PostgreSQL 設計ガイドライン</li>
</ul>
<hr>
<div id="footnotes"><hr><div id="footnotelist"><ol style="list-style:none; padding-left: 0;"><li id="fn:1"><span style="vertical-align: top; padding-right: 10px;">1.</span><span style="vertical-align: top;">通常セマンティック値は英語で表現されることが一般的です。日本企業の伝統的なシステムにおいては英語ではなくローマ字で表現する文化もありますが、この場合はそもそもローマ字自体の可読性が高いのかどうかという議論がありそうです。</span> ↩</li><li id="fn:2"><span style="vertical-align: top; padding-right: 10px;">2.</span><span style="vertical-align: top;">SQL アンチパターン 幻の第 26 章「とりあえず削除フラグ」 でも取り上げられています。</span> ↩</li><li id="fn:3"><span style="vertical-align: top; padding-right: 10px;">3.</span><span style="vertical-align: top;">フラグ値においては「0=OFF, 1=ON」という暗黙の意味付けがあり、その意味では 0/1 による表現もセマンティックな表現と言えるかもしれません。</span> ↩</li></ol></div></div>]]></content>
    <summary type="html">システムにおける「区分」とはカテゴライズ可能な値の集合体を表すものであり、「区分値」とはその集合に属する個々の識別子を指します。たとえば、あるアイテムの「ステータス」という区分には「下書き」「レビュー中」「承認済み」といった区分値が含まれます。</summary>
    <category term="DataEngineering" scheme="https://future-architect.github.io/categories/DataEngineering/"/>
    <category term="データマネジメント" scheme="https://future-architect.github.io/tags/%E3%83%87%E3%83%BC%E3%82%BF%E3%83%9E%E3%83%8D%E3%82%B8%E3%83%A1%E3%83%B3%E3%83%88/"/>
    <category term="区分値" scheme="https://future-architect.github.io/tags/%E5%8C%BA%E5%88%86%E5%80%A4/"/>
    <category term="設計" scheme="https://future-architect.github.io/tags/%E8%A8%AD%E8%A8%88/"/>
  </entry>
  <entry>
    <title>データカタログを中心とした自律分散組織</title>
    <link href="https://future-architect.github.io/articles/20250310a/"/>
    <id>https://future-architect.github.io/articles/20250310a/</id>
    <published>2025-03-09T15:00:00.000Z</published>
    <updated>2025-03-09T15:00:00.000Z</updated>
    <author><name>大前七奈</name></author>
    <content type="html"><![CDATA[<p>データカタログは、自律分散組織を円滑に進める上で重要な役割を果たします。</p>
<p>データカタログ整備を含めたデータマネジメントを専門組織に任せるブームが過去に一時期的にありましたが、この体制があくまで過渡期であり1つの部門に負荷をかけすぎるため、長期的に目指す姿ではありません。</p>
<p>そんな中で、データカタログを中心に置きつつ、各ステークホルダーが自由にデータカタログのプラットフォーム上でGive&amp;Takeし、データの提供と利用へのモチベーションを感じてもらう体制とアーキテクチャづくりこそ持続可能な体制かと思い、今回の記事の執筆に至りました。</p>
<h2 id="背景：-データの所在と管理を分散化">背景： データの所在と管理を分散化</h2><p>データの所有権とマネジメントを分散化することに重点を置いたアーキテクチャ（データメッシュ構造）において、データカタログが中心的な役割を担います。</p>
<p>データカタログの存在により、「データがほしいユーザー」、「データを提供するユーザー」が自由に出会い、「データ管理したいユーザー」が総合的に利用状況、データの質と量を把握することを可能にします。</p>
<p>以下が今回のアーキテクチャ図となります。</p>
<img fetchpriority="high" src="/images/2025/20250310a/image.png" alt="" width="1200" height="682">

<h2 id="必要な時に必要なデータをすべてのユーザーに提供-Just-In-Time">必要な時に必要なデータをすべてのユーザーに提供 (Just In Time)</h2><p>データカタログの価値としては、ユーザーにリアルタイムに情報を提供できる仕組みを可能にすることにあります。さらに日本の労働人口が日々減少し、定型の運用作業に人員を割けなくなっていくなかで、データを探したいユーザーのニーズだけでなく、データを提供したいユーザーやデータを管理する統制部門、システム部門のニーズも今回のアーキテクチャで考慮されております。今回の自助プラットフォームの実装では、様々な連携APIやWebhookを利用しております。</p>
<img src="/images/2025/20250310a/image_2.png" alt="" width="1200" height="684" loading="lazy">

<h2 id="ユーザーにデータ探索と共有の体験を提供">ユーザーにデータ探索と共有の体験を提供</h2><p>リアルタイム連携することで、データカタログの陳腐化を防げると同時に、定型作業のような「面倒くさい」ことを自動化させつつ、ユーザーに最高なデータ探索・共有の体験を提供できます。</p>
<h2 id="あらゆるメタ情報と結びつける">あらゆるメタ情報と結びつける</h2><p>自助プラットフォームの中で、フリーランスのデータエンジニアがコードベースで書いたデータスキーマ、いわゆるデータのメタデータのみならず、データスチュワード&#x2F;データアーキテクトが定めたテーブル権限定義や運用ルール、定期的に集計されるユーザー利用状況などあらゆる情報も取得できます。各データのステークホルダーが各自プル型で情報を取得するようになります。</p>
<p>※我々の新しい取り組み「フリーランスプラットフォームの活用」について、別の記事を参考していただけるとうれしいです（リンク：https://future-architect.github.io/articles/20241029a/ by フューチャーアーキテクト　高瀬陸）</p>
<img src="/images/2025/20250310a/image_3.png" alt="" width="1200" height="683" loading="lazy">

<h2 id="ユーザーの業務とデータの認知マップ">ユーザーの業務とデータの認知マップ</h2><p>あらゆるメタ情報を集約しても、まだ不十分です。なぜならば、ユーザーによって職種や今のIT経験にばらつきがあるからです。そういった性質を持ったユーザーを分類し、ユーザー群ごとに一番詳しい業務用語からデータを検索することで、自然とテーブル情報にたどり着けるように設定しております。</p>
<p>それに加え、ITリテラシーによってポータルサイトの入口を細分化するよう工夫しております。<br><img src="/images/2025/20250310a/image_4.png" alt="" width="1200" height="683" loading="lazy"></p>
<h2 id="自律分散組織を支えるアーキテクチャ">自律分散組織を支えるアーキテクチャ</h2><p>データカタログの自動処理レイヤについては、スキャニングレイヤとソーシャルレイヤで構成され、GCP内の情報を取得して表示したり、UIからの変更をBigQueryに反映したりできます。今回は、ユーザー側では既存のグループウェアNotionをすでに利用されていることに加え、複数軸で情報のビューを表示できることもあり、そのままNotionをデータカタログのUIとして採用することになりました。</p>
<img src="/images/2025/20250310a/image_5.png" alt="" width="1200" height="683" loading="lazy">

<p>以上が今回の自律分散組織をささえるアーキテクチャとそのポイントを紹介させていただきました。なにかヒントになれたら幸いです！</p>
<h2 id="参考文献">参考文献</h2><ul>
<li>大規模データ管理 ―エンタープライズアーキテクチャのベストプラクティス （kindle）</li>
</ul>
]]></content>
    <summary type="html">データカタログは、自律分散組織を円滑に進める上で重要な役割を果たします。データカタログ整備を含めたデータマネジメントを専門組織に任せるブームが過去に一時期的にありましたが、この体制があくまで過渡期であり...</summary>
    <category term="DataEngineering" scheme="https://future-architect.github.io/categories/DataEngineering/"/>
    <category term="データカタログ" scheme="https://future-architect.github.io/tags/%E3%83%87%E3%83%BC%E3%82%BF%E3%82%AB%E3%82%BF%E3%83%AD%E3%82%B0/"/>
    <category term="データガバナンス" scheme="https://future-architect.github.io/tags/%E3%83%87%E3%83%BC%E3%82%BF%E3%82%AC%E3%83%90%E3%83%8A%E3%83%B3%E3%82%B9/"/>
    <category term="データマネジメント" scheme="https://future-architect.github.io/tags/%E3%83%87%E3%83%BC%E3%82%BF%E3%83%9E%E3%83%8D%E3%82%B8%E3%83%A1%E3%83%B3%E3%83%88/"/>
    <category term="組織論" scheme="https://future-architect.github.io/tags/%E7%B5%84%E7%B9%94%E8%AB%96/"/>
  </entry>
  <entry>
    <title>リリース直前にライブラリのインストールエラーが発生した際にどのように対応したか - Glue Python Shell起動エラーの対応</title>
    <link href="https://future-architect.github.io/articles/20240119a/"/>
    <id>https://future-architect.github.io/articles/20240119a/</id>
    <published>2024-01-18T15:00:00.000Z</published>
    <updated>2024-01-18T15:00:00.000Z</updated>
    <author><name>多賀聡一朗</name></author>
    <content type="html"><![CDATA[<img fetchpriority="high" src="/images/2024/20240119a/d6788854-dfac-4797-979e-3f40531a1eee.jpeg" alt="" width="1024" height="1024">

<p>(DALL-E3 で生成)</p>
<h2 id="概要">概要</h2><p>先日、本番リリースを控えたシステムで OSS ライブラリのインストール起因のエラーが発生しました。<br>実際に起きた事象と、どのように検討して対応したのかを残すべく、ポストモーテムの形式で当記事を書きました。</p>
<h2 id="ポストモーテム">ポストモーテム</h2><h3 id="発生日">発生日</h3><p>2023&#x2F;12&#x2F;30</p>
<h3 id="サマリ">サマリ</h3><p>AWS Glue (PythonShell)を起動する際に、awswrangler ライブラリをインストールする工程で、依存関係にある lxml のインストールに失敗しました。その結果、Glue ジョブの起動に失敗しエラーとなりました。</p>
<h4 id="バージョン">バージョン</h4><div class="scroll"><table>
<thead>
<tr>
<th>システム&#x2F;ライブラリ</th>
<th>バージョン</th>
</tr>
</thead>
<tbody><tr>
<td>AWS Glue PythonShell</td>
<td>Glue version&#x3D;3.0 <br>Python version&#x3D;Python3.9<br>DPU&#x3D;0.0625</td>
</tr>
<tr>
<td>awswrangler</td>
<td>1.5.2</td>
</tr>
<tr>
<td>lxml</td>
<td>5.0.0</td>
</tr>
</tbody></table></div>
<h3 id="インパクト">インパクト</h3><p>AWS Glue PythonShell の Libraries 設定に、  </p>
<p>wheel 形式で awswrangler  を指定<br>or<br>wheel形式で awswrangler に依存しているライブラリを指定…</p>
<p>している Glue ジョブが全て実行エラーとなりました。</p>
<h3 id="根本原因">根本原因</h3><p>awswrangler が依存している、lxml ライブラリの最新バージョン <code>5.0.0</code> のインストール時に、以下 2種類のエラーが発生しました。</p>
<figure class="highlight sh"><table><tr><td class="code"><pre><span class="line"><span class="comment"># wheel パッケージで lxml のインストールに失敗</span></span><br><span class="line"></span><br><span class="line">Using legacy <span class="string">&#x27;setup.py install&#x27;</span> <span class="keyword">for</span> lxml, since package <span class="string">&#x27;wheel&#x27;</span> is not installed.</span><br></pre></td></tr></table></figure>

<figure class="highlight sh"><table><tr><td class="code"><pre><span class="line"><span class="comment"># setup.py でインストール時に、</span></span><br><span class="line"><span class="comment"># gcc で src/lxml/etree.c をコンパイルする際に、out of memory エラーが発生</span></span><br><span class="line"></span><br><span class="line">× Running setup.py install <span class="keyword">for</span> lxml did not run successfully.  </span><br><span class="line">│ <span class="built_in">exit</span> code: 1  </span><br><span class="line">╰─&gt; [112 lines of output]</span><br><span class="line">(省略)</span><br><span class="line">    gcc -pthread -Wno-unused-result -Wsign-compare -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -DCYTHON_CLINE_IN_TRACEBACK=0 -I/usr/include/libxml2 -Isrc -Isrc/lxml/includes -I/.pyenv/versions/python39_loaded/include -I/.pyenv/versions/3.9.10/include/python3.9 -c src/lxml/etree.c -o build/temp.linux-x86_64-3.9/src/lxml/etree.o -w  </span><br><span class="line">      </span><br><span class="line">    cc1: out of memory allocating 65536 bytes after a total of 31883264 bytes  </span><br><span class="line">    Compile failed: <span class="built_in">command</span> <span class="string">&#x27;/usr/bin/gcc&#x27;</span> failed with <span class="built_in">exit</span> code 1  </span><br><span class="line">    creating tmp  </span><br><span class="line">    cc -I/usr/include/libxml2 -I/usr/include/libxml2 -c /tmp/xmlXPathInit3c3vwixm.c -o tmp/xmlXPathInit3c3vwixm.o  </span><br><span class="line">    cc tmp/xmlXPathInit3c3vwixm.o -lxml2 -o a.out  </span><br><span class="line">    error: <span class="built_in">command</span> <span class="string">&#x27;/usr/bin/gcc&#x27;</span> failed with <span class="built_in">exit</span> code 1  </span><br><span class="line">    [end of output]  </span><br><span class="line">  </span><br><span class="line">note: This error originates from a subprocess, and is likely not a problem with pip.  </span><br><span class="line">error: legacy-install-failure</span><br><span class="line">× Encountered error <span class="keyword">while</span> trying to install package.  </span><br><span class="line">╰─&gt; lxml</span><br></pre></td></tr></table></figure>

<h3 id="発生要因">発生要因</h3><p>lxml 最新バージョン <code>5.0.0</code> が、2023&#x2F;12&#x2F;30 05:29 (JST) にリリースされたためです。</p>
<h3 id="検出">検出</h3><p>Glue ジョブの実行時に、 <code>Internal service error</code> 、タイムアウトエラー、もしくは以下のエラーが発生しました。</p>
<figure class="highlight text"><table><tr><td class="code"><pre><span class="line">CommandFailedException: /tmp/glue-python-libs-XXXX/&lt;ライブラリ&gt;.whl installation failed after 2th retry due to exception: CalledProcessError</span><br></pre></td></tr></table></figure>

<h3 id="対応">対応</h3><h4 id="対応案の検討">対応案の検討</h4><p>以下の A と B、 2 つの対応案を検討しました。</p>
<h5 id="A-awswrangler-を-AWS-Glue-PythonShell-で事前にパッケージ済みのライブラリ利用へ変更">A. awswrangler を AWS Glue PythonShell で事前にパッケージ済みのライブラリ利用へ変更</h5><p>今回利用していた <code>awswrangler==1.5.2</code> は、AWS Glue PythonShell において事前にパッケージ済みのライブラリセットに含まれています。</p>
<p>参考: AWS Glueでの Python シェルジョブ - AWS Glue (amazon.com)</p>
<p>そのため、 <code>awswrangler</code> を明示的にインストールせずとも利用できるため、<br>AWS Glue ジョブの Libraries 設定から awswrangler の指定を削除することで、起動時のインストールを防ぎ、ライブラリの利用継続を行う案です。</p>
<p>メリット</p>
<ul>
<li>AWS 側で用意されたライブラリが利用でき、一般に利用できることから、ライブラリの状態は枯れている</li>
<li>起動時のインストール処理が省略され、実処理を実行するまでの時間が早くなり、全体的な処理速度向上が見込める<ul>
<li>※ 最大30秒程度の短縮であるため、全体のジョブ実行時間次第で支配的かどうか判断する</li>
</ul>
</li>
</ul>
<p>デメリット</p>
<ul>
<li>単体テスト&#x2F;結合テスト時にインストールしていたバージョンと、 <code>awswrangler==1.5.2</code> に依存する他パッケージのバージョンに差異が生まれる可能性がある</li>
</ul>
<h5 id="B-Glue-ジョブ設定の、DPU-0-0625-から-DPU-1-へ設定変更">B. Glue ジョブ設定の、DPU&#x3D;0.0625 から DPU&#x3D;1 へ設定変更</h5><p>根本原因にて記載した通り、lxmlを setup.py にてインストールする際に、<code>out of memory</code> エラーが発生していました。<br>そのため、マシンスペックを上げることで解消できないかと推測し、検証環境にて DPU&#x3D;0.0625 → DPU&#x3D;1 へ変更して実行したところ、正常に処理が完了しました。</p>
<p>メリット</p>
<ul>
<li>エラーが発生する前のライブラリのバージョン状態に近い環境で、Glue ジョブを動作させることができる<ul>
<li>awswranglerに依存するライブラリのバージョンをエラー直前の成功したバージョンに近い状態とできる<ul>
<li>※ 近いという表現は、設定次第で依存する他のライブラリバージョンも変更される可能性があることに由来する(後述するが、そもそも実行時にバージョンが揺れること自体が本当の根本原因である)</li>
</ul>
</li>
</ul>
</li>
</ul>
<p>デメリット</p>
<ul>
<li>ビルドして作成する必要があり、wheel でのインストールに失敗する lxmlのバージョンが利用される</li>
<li>DPU 値を上げるため、金銭的なコストが増加する</li>
</ul>
<h4 id="対応案の選択">対応案の選択</h4><p>結論、対応案 A を選択しました。</p>
<p>判断基準として、”不具合を追加で発生させないこと” を置きました。<br>システム性質上、本番環境での継続的な実行ではなく、数回の実行を安定して行うことが求められていました。その結果、ライブラリの最新バージョン追従よりも、成熟し安定したバージョンでの実行を優先しました。</p>
<p>リスク評価としては、インストールエラーが発生した、最新の lxml により発生する不具合リスクと、awswranglerに依存するライブラリのバージョンが下がることで発生する不具合のリスクを考慮し、定量的ではないですが後者のほうがより発生するリスクが低いと判断しました。</p>
<p>ただ、どちらにせよリスクは発生するため、単体テストと AWS 環境上での結合テストを再度実行し、動作保証の上で本番稼働しました。</p>
<h4 id="修正事項">修正事項</h4><p>AWS Glue ジョブの Libraries 設定に指定している wheel ライブラリ内で、awswrangler をインストールしない設定へ修正しました。<br>具体的には、 <code>poetry build -f wheel</code> で独自ライブラリを wheel ファイルへビルドしていたため、awswrangler を <code>poertry</code> における <code>dev group</code>   の依存へ変更しました。</p>
<p>before</p>
<figure class="highlight toml"><table><tr><td class="code"><pre><span class="line"><span class="comment"># pyproject.toml</span></span><br><span class="line"></span><br><span class="line"><span class="section">[tool.poetry.dependencies]</span></span><br><span class="line"><span class="attr">awswrangler</span> = <span class="string">&quot;2.15.1&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="section">[tool.poetry.group.dev.dependencies]</span></span><br></pre></td></tr></table></figure>

<p>after</p>
<figure class="highlight toml"><table><tr><td class="code"><pre><span class="line"><span class="comment"># pyproject.toml</span></span><br><span class="line"></span><br><span class="line"><span class="section">[tool.poetry.dependencies]</span></span><br><span class="line"></span><br><span class="line"><span class="section">[tool.poetry.group.dev.dependencies]</span></span><br><span class="line"><span class="attr">awswrangler</span> = <span class="string">&quot;2.15.1&quot;</span> <span class="comment"># dev group へ変更</span></span><br></pre></td></tr></table></figure>

<h3 id="学び">学び</h3><h4 id="うまくいったこと">うまくいったこと</h4><p>幸いなことに、単体テスト&#x2F;結合テストにて追加の問題が発生せず、本番環境での実行も安定していました。</p>
<h4 id="振り返り">振り返り</h4><p>後から振り返ると、リスク判断において正確な要素を抽出できていなかった点がありました。<br>その点は、lxml に対して実際に依存している処理があったのか否かです。lxmlへの依存は、awswrangler (正確には、内部で利用している <code>pandas</code> )内の処理のため、ライブラリの実装を読む必要がありましたが、正確なリスク評価としては必要だったのではと思いました。<br>時間的余裕次第ですが、調査をする選択肢をもっておくべきでした。</p>
<p>また、案の比較時に、インストールエラーが発生した lxml を利用すること自体が心理的障壁となり、選択時にバイアスがかかってしまったという点もありました。これは、エラーが発生したバージョンを使いたくないという感情から来るものでした。<br>客観的な判断で、より正確にリスク評価ができるとより良かったなと感じています。</p>
<h4 id="そもそもの話">そもそもの話</h4><p>根本的には、実行時のライブラリのバージョンが固定されていないことが発生の要因です。<br>バージョンが固定 “されている”・”されていない” (※1) で少しメリット・デメリットを考えてみると、<br>メリットとしては、設定次第ではありますが最新のバージョンに近いバージョンが選択されることで、脆弱性の修正や性能向上等のバージョンアップによる改善を取り込むことができます。<br>デメリットとしては、当事象のようにバージョンが揺れることで、テスト時に発生していなかった不具合が突然発生するリスクが生まれます。</p>
<p>ケースバイケースではありますが、安定して運用することが第一である場合はデメリットは許容できないことが多い印象です。<br>そのため、ライブラリのバージョンを lock ファイル等を利用して固定の上で安定運用しつつ、もし継続して利用されるシステムの場合、適切にメンテナンスして如何にバージョンを上げていくかを考えることになるのかなと思います。(余談ですが、メンテナンスが検討できておらず、塩漬けにされて、いざ大きめの脆弱性が発見され、いきなり対応できないといった問題はありがちな気がします。)</p>
<p>ちなみに、AWS Glue の追加ライブラリにおいて、lock ファイル利用したライブラリのバージョンを固定する方法は現時点ではわかりませんでした。</p>
<p><strong>※1 <code>バージョンが固定されていない</code> とは、ライブラリに対する依存ライブラリ設定に沿っている上という前提になります。</strong></p>
<p>例えば、pyproject.toml - awswrangler の設定を参考にすると、以下の通りです。</p>
<ul>
<li>boto3 は <code>1.20.32</code> 以上のバージョンに依存</li>
<li>pandas の場合<ul>
<li>Python version が 3.9 より小さい場合は、 <code>1.2.0 &lt;= version &lt; 2.1.0</code> の間のバージョンに依存</li>
<li>Python version が 3.9 以上の場合は、 <code>1.2.0 &lt;= version &lt; 3.0.0</code> の間のバージョンに依存</li>
</ul>
</li>
</ul>
<figure class="highlight toml"><table><tr><td class="code"><pre><span class="line"><span class="comment"># 参考: https://github.com/aws/aws-sdk-pandas/blob/9d5c1a67af3f5e7e8d7aa506da1b97277d3e3bd6/pyproject.toml#L28</span></span><br><span class="line"></span><br><span class="line"><span class="section">[tool.poetry.dependencies]</span></span><br><span class="line"><span class="attr">python</span> = <span class="string">&quot;&gt;=3.8, &lt;4.0&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># Required</span></span><br><span class="line"><span class="attr">boto3</span> = <span class="string">&quot;^1.20.32&quot;</span></span><br><span class="line"><span class="attr">botocore</span> = <span class="string">&quot;^1.23.32&quot;</span></span><br><span class="line"><span class="attr">pandas</span> = [</span><br><span class="line">    &#123; version = <span class="string">&quot;&gt;=1.2.0,&lt;2.1.0&quot;</span>, markers = <span class="string">&quot;python_version &lt; \&quot;3.9\&quot;&quot;</span> &#125;,</span><br><span class="line">    &#123; version = <span class="string">&quot;&gt;=1.2.0,&lt;3.0.0&quot;</span>, markers = <span class="string">&quot;python_version &gt;= \&quot;3.9\&quot;&quot;</span> &#125;,</span><br><span class="line">]</span><br></pre></td></tr></table></figure>

<p>上記設定のため、boto3 であれば実行時に <code>1.20.32</code> 以上のバージョン (最新バージョン) が利用され、pandas もバージョン指定内の最新バージョンが利用される可能性があります。</p>
<h2 id="補足">補足</h2><h3 id="lxml-の-wheel-インストールエラー自体の解消">lxml の wheel インストールエラー自体の解消</h3><p>バージョン 5.0.1 と 5.1.0 にて、解消されたバージョンがリリースされています。</p>
<p>参考: lxml changelog<br><img src="/images/2024/20240119a/image.png" alt="image.png" width="1200" height="358" loading="lazy"></p>
<h3 id="wheel-とは">wheel とは</h3><p>PEP 427 で定義された Python のパッケージファイルフォーマットを指します。拡張子は <code>.whl</code> であり、中身としては ZIP 形式のアーカイブです。</p>
<p>フォルダ構成は、ZIP 解凍をすることでわかりますが、以下の通りです。</p>
<figure class="highlight sh"><table><tr><td class="code"><pre><span class="line"></span><br><span class="line">├─ &lt;ライブラリディレクトリ&gt;</span><br><span class="line">   ├─ ...</span><br><span class="line">└─ &lt;ライブラリ名&gt;.dist-info</span><br><span class="line">   ├─ METADATA</span><br><span class="line">   ├─ RECORD</span><br><span class="line">   └─ WHEEL</span><br></pre></td></tr></table></figure>

<p>当事象に関連する内容としては、 <code>.dist-info</code> 以下の <code>METADATA</code> に依存するライブラリのバージョンが記載されています。<br><code>pip</code> で <code>wheel</code> ファイルを指定してインストールした際に、<code>Requires-Dist</code> に記載されているライブラリがインストールされます。</p>
<figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">Metadata-Version: 2.1</span><br><span class="line">Name: &lt;ライブラリ名&gt;</span><br><span class="line">Version: 1.0.0</span><br><span class="line">Summary: </span><br><span class="line">Author: XXX</span><br><span class="line">Requires-Python: &gt;=3.9.10,&lt;3.11</span><br><span class="line">Classifier: Programming Language :: Python :: 3</span><br><span class="line">Classifier: Programming Language :: Python :: 3.10</span><br><span class="line">Requires-Dist: boto3 (==1.28.64)</span><br><span class="line">...</span><br><span class="line"></span><br><span class="line">Description-Content-Type: text/markdown</span><br><span class="line"></span><br><span class="line">(README.md の内容が記載)</span><br></pre></td></tr></table></figure>

<h2 id="所感">所感</h2><p>ポストモーテムとして、振り返りをまとめてみました。<br>数年に1回あるかないかの問題ではありますが、同様の問題が発生した際に判断の参考になれば良いなと思います。</p>
<h2 id="参考">参考</h2><ul>
<li>ポストモーテムを理解する #運用 - Qiita</li>
<li>lxml changelog</li>
</ul>
]]></content>
    <summary type="html">先日、本番リリースを控えたシステムで OSS ライブラリのインストール起因のエラーが発生しました。実際に起きた事象と、どのように検討して対応したのかを残すべく、ポストモーテムの形式で当記事を書きました。</summary>
    <category term="DataEngineering" scheme="https://future-architect.github.io/categories/DataEngineering/"/>
    <category term="AWS" scheme="https://future-architect.github.io/tags/AWS/"/>
    <category term="Glue" scheme="https://future-architect.github.io/tags/Glue/"/>
    <category term="Glue Python Shell" scheme="https://future-architect.github.io/tags/Glue-Python-Shell/"/>
    <category term="Python" scheme="https://future-architect.github.io/tags/Python/"/>
    <category term="トラブルシュート" scheme="https://future-architect.github.io/tags/%E3%83%88%E3%83%A9%E3%83%96%E3%83%AB%E3%82%B7%E3%83%A5%E3%83%BC%E3%83%88/"/>
    <category term="振り返り" scheme="https://future-architect.github.io/tags/%E6%8C%AF%E3%82%8A%E8%BF%94%E3%82%8A/"/>
  </entry>
  <entry>
    <title>タグを利用したBigQueryのアクセス制御</title>
    <link href="https://future-architect.github.io/articles/20231018a/"/>
    <id>https://future-architect.github.io/articles/20231018a/</id>
    <published>2023-10-17T15:00:00.000Z</published>
    <updated>2023-10-17T15:00:00.000Z</updated>
    <author><name>岸下優介</name></author>
    <content type="html"><![CDATA[<h2 id="はじめに">はじめに</h2><p>BigQueryは完全マネージドな、ペタバイトスケールかつコスパのよいデータウェアハウスとして知られております。そのため、ほぼリアルタイムで膨大な量のデータを解析することを可能としております。</p>
<p>便利なツールである一方、BigQueryで取り扱うデータには個人情報が含まれていることもあり、適切なアクセス制御が望まれます。<br>本記事では、Resource Managerのタグ機能を利用して、Terraformによるアクセス制御の実装を紹介したいと思います。</p>
<h2 id="Resource-Managerのタグとは">Resource Managerのタグとは</h2><p>Google Cloudのリソースに対して、key-valueペアでタグを付与してIAMの条件に含めることができる機能です。</p>
<p>例えばBigQueryのデータセットであれば、Key:<code>environment</code>に対してValue:<code>dev</code>, <code>stg</code>, <code>prd</code>を用意したり、Key:<code>dataset_type</code>に対してValue:<code>non-pii</code>, <code>pii</code>（個人情報を含むか否か）を用意したりなど、データの種類に応じてタグを付与して、より詳細な条件でIAMを管理できます。</p>
<p>タグの作成と管理 - Google Cloud</p>
<h2 id="KeyとValueを作ってみる">KeyとValueを作ってみる</h2><p>早速Terraformを書いていきます。</p>
<p>タグはOrganization配下での管理となります。まずはKeyを作ります。</p>
<figure class="highlight sh"><figcaption><span>tags_tag_key.tf</span></figcaption><table><tr><td class="code"><pre><span class="line">resource <span class="string">&quot;google_tags_tag_key&quot;</span> <span class="string">&quot;env_key&quot;</span> &#123;</span><br><span class="line">  parent      = <span class="string">&quot;organizations/<span class="variable">$&#123;local.organization.id&#125;</span>&quot;</span></span><br><span class="line">  short_name  = <span class="string">&quot;environment&quot;</span></span><br><span class="line">  description = <span class="string">&quot;Environment key&quot;</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>このKeyに対してValueを作ります。</p>
<figure class="highlight sh"><figcaption><span>tags_tag_value.tf</span></figcaption><table><tr><td class="code"><pre><span class="line">resource <span class="string">&quot;google_tags_tag_value&quot;</span> <span class="string">&quot;dev_tag&quot;</span> &#123;</span><br><span class="line">  parent      = <span class="string">&quot;tagKeys/<span class="variable">$&#123;google_tags_tag_key.env_key.name&#125;</span>&quot;</span></span><br><span class="line">  short_name  = <span class="string">&quot;dev&quot;</span></span><br><span class="line">  description = <span class="string">&quot;Development tag&quot;</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">resource <span class="string">&quot;google_tags_tag_value&quot;</span> <span class="string">&quot;stg_tag&quot;</span> &#123;</span><br><span class="line">  parent      = <span class="string">&quot;tagKeys/<span class="variable">$&#123;google_tags_tag_key.env_key.name&#125;</span>&quot;</span></span><br><span class="line">  short_name  = <span class="string">&quot;stg&quot;</span></span><br><span class="line">  description = <span class="string">&quot;Staging tag&quot;</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">resource <span class="string">&quot;google_tags_tag_value&quot;</span> <span class="string">&quot;prd_tag&quot;</span> &#123;</span><br><span class="line">  parent      = <span class="string">&quot;tagKeys/<span class="variable">$&#123;google_tags_tag_key.env_key.name&#125;</span>&quot;</span></span><br><span class="line">  short_name  = <span class="string">&quot;prd&quot;</span></span><br><span class="line">  description = <span class="string">&quot;Production tag&quot;</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>適用後、コンソールを見てみましょう。<br><img fetchpriority="high" src="/images/2023/20231018a/71638260-a888-a69d-56d2-bcb92fb94825.png" alt="" width="1200" height="489"></p>
<p><code>environment</code>に対して、<code>dev</code>, <code>stg</code>, <code>prd</code>というkey-valueペアが生成されました。</p>
<h2 id="BigQueryのDatasetにタグを付与する">BigQueryのDatasetにタグを付与する</h2><p>今回生成したタグをBigQueryのDatasetに付与していきましょう。<br>まずは以下のデータセットを用意します。</p>
<figure class="highlight sh"><figcaption><span>bigquery.tf</span></figcaption><table><tr><td class="code"><pre><span class="line">resource <span class="string">&quot;google_bigquery_dataset&quot;</span> <span class="string">&quot;dataset_dev&quot;</span> &#123;</span><br><span class="line">  project    = google_project.project.project_id</span><br><span class="line">  dataset_id = <span class="string">&quot;dataset_dev&quot;</span></span><br><span class="line">  location   = <span class="string">&quot;asia-northeast1&quot;</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">resource <span class="string">&quot;google_bigquery_dataset&quot;</span> <span class="string">&quot;dataset_stg&quot;</span> &#123;</span><br><span class="line">  project    = google_project.project.project_id</span><br><span class="line">  dataset_id = <span class="string">&quot;dataset_stg&quot;</span></span><br><span class="line">  location   = <span class="string">&quot;asia-northeast1&quot;</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">resource <span class="string">&quot;google_bigquery_dataset&quot;</span> <span class="string">&quot;dataset_prd&quot;</span> &#123;</span><br><span class="line">  project    = google_project.project.project_id</span><br><span class="line">  dataset_id = <span class="string">&quot;dataset_prd&quot;</span></span><br><span class="line">  location   = <span class="string">&quot;asia-northeast1&quot;</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>データセットへタグを付与する方法ですが、</p>
<ul>
<li>Terraform</li>
<li>gcloudコマンド</li>
<li>Google Cloudコンソール</li>
</ul>
<p>の3つの方法が存在します。</p>
<h3 id="Terraformでタグを付与する">Terraformでタグを付与する</h3><p>Terraformでは、<code>google_tags_location_tag_binding</code>を利用してタグを付与します。<br>※本リソースは、現時点（2023&#x2F;10&#x2F;15）ではGoogle Betaとなっております。</p>
<p>Terraform公式 - google_tags_location_tag_binding</p>
<figure class="highlight sh"><figcaption><span>tags_location_tag_binding.tf</span></figcaption><table><tr><td class="code"><pre><span class="line">data <span class="string">&quot;google_tags_tag_key&quot;</span> <span class="string">&quot;env_key&quot;</span> &#123;</span><br><span class="line">  parent     = <span class="string">&quot;organizations/<span class="variable">$&#123;local.organization.id&#125;</span>&quot;</span></span><br><span class="line">  short_name = <span class="string">&quot;environment&quot;</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">data <span class="string">&quot;google_tags_tag_value&quot;</span> <span class="string">&quot;dev_tag&quot;</span> &#123;</span><br><span class="line">  parent     = <span class="string">&quot;tagKeys/<span class="variable">$&#123;data.google_tags_tag_key.env_key.name&#125;</span>&quot;</span></span><br><span class="line">  short_name = <span class="string">&quot;dev&quot;</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">data <span class="string">&quot;google_tags_tag_value&quot;</span> <span class="string">&quot;stg_tag&quot;</span> &#123;</span><br><span class="line">  parent     = <span class="string">&quot;tagKeys/<span class="variable">$&#123;data.google_tags_tag_key.env_key.name&#125;</span>&quot;</span></span><br><span class="line">  short_name = <span class="string">&quot;stg&quot;</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">data <span class="string">&quot;google_tags_tag_value&quot;</span> <span class="string">&quot;prd_tag&quot;</span> &#123;</span><br><span class="line">  parent     = <span class="string">&quot;tagKeys/<span class="variable">$&#123;data.google_tags_tag_key.env_key.name&#125;</span>&quot;</span></span><br><span class="line">  short_name = <span class="string">&quot;prd&quot;</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">resource <span class="string">&quot;google_tags_location_tag_binding&quot;</span> <span class="string">&quot;dev&quot;</span> &#123;</span><br><span class="line">  provider  = google-beta</span><br><span class="line">  parent    = <span class="string">&quot;//bigquery.googleapis.com/projects/<span class="variable">$&#123;google_project.project_one.project_id&#125;</span>/datasets/<span class="variable">$&#123;google_bigquery_dataset.dataset_dev.dataset_id&#125;</span>&quot;</span></span><br><span class="line">  tag_value = data.google_tags_tag_value.dev_tag.id</span><br><span class="line">  location  = <span class="string">&quot;asia-northeast1&quot;</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">resource <span class="string">&quot;google_tags_location_tag_binding&quot;</span> <span class="string">&quot;stg&quot;</span> &#123;</span><br><span class="line">  provider  = google-beta</span><br><span class="line">  parent    = <span class="string">&quot;//bigquery.googleapis.com/projects/<span class="variable">$&#123;google_project.project_one.project_id&#125;</span>/datasets/<span class="variable">$&#123;google_bigquery_dataset.dataset_stg.dataset_id&#125;</span>&quot;</span></span><br><span class="line">  tag_value = data.google_tags_tag_value.stg_tag.id</span><br><span class="line">  location  = <span class="string">&quot;asia-northeast1&quot;</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">resource <span class="string">&quot;google_tags_location_tag_binding&quot;</span> <span class="string">&quot;prd&quot;</span> &#123;</span><br><span class="line">  provider  = google-beta</span><br><span class="line">  parent    = <span class="string">&quot;//bigquery.googleapis.com/projects/<span class="variable">$&#123;google_project.project_one.project_id&#125;</span>/datasets/<span class="variable">$&#123;google_bigquery_dataset.dataset_prd.dataset_id&#125;</span>&quot;</span></span><br><span class="line">  tag_value = data.google_tags_tag_value.prd_tag.id</span><br><span class="line">  location  = <span class="string">&quot;asia-northeast1&quot;</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<h3 id="gcloudコマンドでタグを付与する">gcloudコマンドでタグを付与する</h3><p>以下のコマンドで付与できます。</p>
<figure class="highlight bash"><figcaption><span>terminal</span></figcaption><table><tr><td class="code"><pre><span class="line">gcloud alpha resource-manager tags bindings create \</span><br><span class="line">    --tag-value=&lt;ORGANIZATION_ID&gt;/environment/dev \</span><br><span class="line">    --parent=//bigquery.googleapis.com/projects/my_project/datasets/dataset_dev \</span><br><span class="line">    --location=asia-northeast1</span><br></pre></td></tr></table></figure>

<p>権限が不足している場合は、Organizationにて以下の権限が必要になります。</p>
<ul>
<li>roles&#x2F;resourcemanager.tagUser</li>
</ul>
<h3 id="コンソールでタグを付与する">コンソールでタグを付与する</h3><p>BigQueryのページから、データセットをクリックすると以下のようなデータセット情報が表示されます。<br>この画面から詳細を編集に移動して下さい。</p>
<img src="/images/2023/20231018a/fe171bd5-fe90-8ef9-cb20-1eeb945b2560.png" alt="" width="1200" height="499" loading="lazy">

<p>タグを追加を押すことで、所望のタグを付与できます。</p>
<img src="/images/2023/20231018a/4358d88e-757f-d924-3c7f-a6a0a59ae98c.png" alt="" width="852" height="1222" loading="lazy">

<p>付与されたタグは「タグ」の箇所に記載されるようになります。</p>
<img src="/images/2023/20231018a/a310bef6-a3e4-fa3f-713c-4118f1f30511.png" alt="" width="1200" height="492" loading="lazy">

<h2 id="IAMを付与する">IAMを付与する</h2><p>データセットにタグも付与できたので、最後にIAMを付与しましょう。<br>IAM条件も記載し、<code>dev</code>タグが一致するデータセットのみを閲覧許可します。</p>
<figure class="highlight sh"><figcaption><span>project_iam_member.tf</span></figcaption><table><tr><td class="code"><pre><span class="line">resource <span class="string">&quot;google_project_iam_member&quot;</span> <span class="string">&quot;test_user&quot;</span> &#123;</span><br><span class="line">  project = google_project.project.project_id</span><br><span class="line">  role    = <span class="string">&quot;roles/bigquery.dataViewer&quot;</span></span><br><span class="line">  member  = <span class="string">&quot;user:test@xxx.com&quot;</span></span><br><span class="line">  condition &#123;</span><br><span class="line">    title       = <span class="string">&quot;only-dev&quot;</span></span><br><span class="line">    expression  = <span class="string">&quot;resource.matchTag(\&quot;<span class="variable">$&#123;local.organization.id&#125;</span>/environment\&quot;, \&quot;dev\&quot;)&quot;</span></span><br><span class="line">    description = <span class="string">&quot;Only view the dataset with tag of dev.&quot;</span></span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>適用後、BigQueryを見てみるとちゃんとdevのデータセットのみが見えていることがわかります。</p>
<img src="/images/2023/20231018a/aab47fe7-e64b-ad96-2772-2434b2a716fd.png" alt="" width="1200" height="616" loading="lazy">

<h2 id="まとめ">まとめ</h2><p>本記事ではタグを利用したBigQueryデータセットのアクセス権限制御について紹介しました。</p>
<p>データセット1つ1つのアクセス制御を行うには、各データセットに対してIAM(roles&#x2F;bigquery.dataViewerなど)を割り当てる必要があったのですが、データセットに付与されたタグでまとめてIAMを管理できると適切な粒度でアクセス制御ができ、タグで閲覧可能な範囲がまとめられるので管理も楽できるなーと思いました。</p>
<p>データのセキュリティを強固にするには、内部のメンバーに対してのデータアクセス制御も非常に重要となります。ぜひタグベースのIAM制御を試してみてはいかがでしょうか？</p>
]]></content>
    <summary type="html">BigQueryは完全マネージドな、ペタバイトスケールかつコスパのよいデータウェアハウスとして知られております。便利なツールである一方、BigQueryで取り扱うデータには個人情報が含まれていることもあり、適切なアクセス制御が望まれます。Resource Managerのタグ機能を利用して…</summary>
    <category term="DataEngineering" scheme="https://future-architect.github.io/categories/DataEngineering/"/>
    <category term="BigQuery" scheme="https://future-architect.github.io/tags/BigQuery/"/>
    <category term="GoogleCloud" scheme="https://future-architect.github.io/tags/GoogleCloud/"/>
    <category term="IAM" scheme="https://future-architect.github.io/tags/IAM/"/>
    <category term="アクセス制御" scheme="https://future-architect.github.io/tags/%E3%82%A2%E3%82%AF%E3%82%BB%E3%82%B9%E5%88%B6%E5%BE%A1/"/>
  </entry>
  <entry>
    <title>【合格記】Google Cloud Professional Data Engineer認定資格を振り返る【2023年度版】</title>
    <link href="https://future-architect.github.io/articles/20230915a/"/>
    <id>https://future-architect.github.io/articles/20230915a/</id>
    <published>2023-09-14T15:00:00.000Z</published>
    <updated>2023-09-14T15:00:00.000Z</updated>
    <author><name>岸下優介</name></author>
    <content type="html"><![CDATA[<img fetchpriority="high" src="/images/2023/20230915a/image.png" alt="" width="599" height="586">

<h2 id="はじめに">はじめに</h2><p>TIG 岸下です。</p>
<p>最近Data Engineeringを扱うプロジェクトへ異動したこともあり、Google CloudにおけるData Engineeringを網羅的に学びたく、Professional Data Engineer認定資格を受けてきました。結果として、無事に合格を果たすことができました。</p>
<p>本記事では学習内容などの過程を書いていこうと思います。</p>
<p>また本試験はGoogle Cloudパートナー企業向けのバウチャーを活用して受験しました。大変感謝しております！</p>
<p>過去記事：</p>
<ul>
<li>【合格記】Google Cloud Professional Data Engineer認定資格を振り返る</li>
</ul>
<p>Google Cloud 認定資格関連の過去記事：</p>
<ul>
<li>【合格記】Google Cloud Professional Machine Learning Engineer認定資格を振り返る</li>
<li>Google Cloud Professional Cloud Architectの再認定に合格しました</li>
<li>GCP Professional Cloud Network Engineer に合格しました</li>
<li>GCP Associate Cloud Engineer 合格記</li>
</ul>
<h2 id="試験と出題範囲">試験と出題範囲</h2><p>公式の出題範囲と、実際の試験内容の所感は以下になります。</p>
<h3 id="データ処理システムの設計">データ処理システムの設計</h3><ul>
<li>データの前処理を行ううえでどのようなアーキテクチャ・サービスが推奨されるか？<ul>
<li>ローコード・ノーコードで前処理したい<ul>
<li>DataprepやData Fusion</li>
</ul>
</li>
<li>Kubernetesを利用したオーケストレーションの活用<ul>
<li>Cloud Composer</li>
</ul>
</li>
</ul>
</li>
<li>データの受け口にキューイングシステムであるPubSubを構えておくのが鉄板</li>
<li>データベースの用途は何なのか？ 取り扱うデータは？ 時系列？ トランザクション？<ul>
<li>用途によって、DBのサービスを選ぶ</li>
<li>Cloud SQL&#x2F;Bigtable&#x2F;Firestore&#x2F;Spannerどれを選ぶべきか</li>
</ul>
</li>
<li>サーバーレスでSQL使いたい？ データウェアハウスを構築したい？<ul>
<li>BigQuery使おう</li>
</ul>
</li>
<li>BigQueryとPubSubが優秀<ul>
<li>回答に困ったらとりあえずBigQueryかPubSubが入っている選択肢を選ぶというくらいにBigQueryとPubSubが強すぎる感（主観）</li>
<li>特にBigQueryは別格感ありますね。実務でも使い倒されてます。</li>
</ul>
</li>
</ul>
<h3 id="機械学習モデルの運用化">機械学習モデルの運用化</h3><ul>
<li>モデルが過学習している場合はどうするか？<ul>
<li>データを増やす</li>
<li>正規化する</li>
<li>学習パラメータを減らす</li>
</ul>
</li>
<li>欠損値を含む場合はどうするか？<ul>
<li>ノーコードで除去したい場合はDataprep</li>
<li>DataflowでBigQueryを前処理として組み込む手もある</li>
</ul>
</li>
<li>機械学習モデルの学習処理を早く終わらせたい場合はどうするか？<ul>
<li>GPUが使えるフレームワークであれば、GPU搭載インスタンスの利用</li>
</ul>
</li>
<li>クライアントが解決したい課題はどのモデルを使うべきか<ul>
<li>回帰問題&#x2F;分類問題を理解しておく</li>
</ul>
</li>
<li>機械学習モデルの運用方法<ul>
<li>学習の自動化</li>
<li>データセットの監視・管理</li>
</ul>
</li>
</ul>
<h3 id="ソリューションの品質の確保">ソリューションの品質の確保</h3><ul>
<li>適切なアラート設計<ul>
<li>何を指標としたアラートを設計すべきか？</li>
</ul>
</li>
<li>システムの可用性はどうあるべきか<ul>
<li>ゾーナル&#x2F;リージョナル&#x2F;マルチリージョナル</li>
<li>レプリカの活用</li>
</ul>
</li>
<li>Dataflowなどにおける処理遅延に対するトラブルシューティング<ul>
<li>適切なノードのスケールアップ</li>
</ul>
</li>
<li>法規制に対応するためのデータの置き方<ul>
<li>1プロジェクトにデータを集約させるデータレイクのような形をとっておく</li>
</ul>
</li>
<li>適切なログの集約・保管<ul>
<li>Log sinkやログバケットの活用</li>
<li>検索性を高めたいのであればBigQueryへシンク</li>
</ul>
</li>
</ul>
<h3 id="データ処理システムの構築と運用化">データ処理システムの構築と運用化</h3><ul>
<li>複数のリソースからデータを参照したい場合のDB・ストレージの選択</li>
<li>IAMを利用したデータのアクセス制限<ul>
<li>プロジェクトレベルのIAM</li>
<li>データセットレベルのIAM</li>
<li>テーブルの列レベルのIAM</li>
</ul>
</li>
<li>Cloud DLPを利用した機密情報の保護<ul>
<li>暗号化した情報は復元できるようにしておく必要がある or Not?</li>
</ul>
</li>
</ul>
<h2 id="受験までの過程">受験までの過程</h2><p>勉強期間は約1か月ほどで、主に以下2つの教材を利用しました。</p>
<ul>
<li>Google Cloud Skills Boost for Partners<ul>
<li>Create and Manage Cloud Resources</li>
<li>Perform Foundational Data, ML and AI Tasks in Google Cloud</li>
<li>Engineer Data in Google Cloud</li>
</ul>
</li>
<li>Udemy<ul>
<li>GCP : Google Cloud Professional Data Engineer Practice Tests</li>
</ul>
</li>
</ul>
<h3 id="Google-Cloud-Skills-Boost-for-Partners">Google Cloud Skills Boost for Partners</h3><p>こちらの教材は、Google CloudにおけるData Engineeringをハンズオン形式で構築しながら学ぶことができます。<br>例えば、</p>
<ul>
<li>CSV形式のデータセットに対して、欠損値や外れ値などをDataprepやDataFusionを利用してデータクレンジングを行う</li>
<li>BigQueryを利用して前処理を施した後に、BigQuery MLを利用して機械学習モデルを構築する</li>
<li>Cloud Dataflowを利用した機械学習パイプラインの構築</li>
</ul>
<p>などを、実際のGoogle Cloud環境を利用して構築できます。<br>やはり、手を動かしながら学ぶ方法が一番頭に入ってくると思います。</p>
<h4 id="Udemy">Udemy</h4><p>認定資格には実技試験はないので、やはり仕上げはUdemyの模擬試験で慣らします。</p>
<p>Udemyの模擬試験は色々あって迷うと思いますが、Google Cloudは日々アップデートされておりますので、できるだけ最新の情報が反映された模擬試験（教材が公開されて日が浅いもの）を利用したほうがよいです。</p>
<p>今回利用した模擬試験は英語試験用でしたが、DeepLなどの翻訳機を併用することで難なくこなすことができます。</p>
<p>模擬試験を2～3周やっておけば、自信をもって試験に臨むことができると思います。</p>
<h2 id="おわりに">おわりに</h2><p>本試験はML Engineer認定資格と学習範囲が被っている部分もあり、自分にとってはサクサク学習を進めることができました。ただ、より込み入ったデータ処理の技術、特にCloud ComposerやDataproc周りの知識が浅く、そこの学習を重点的に行いました。これらの範囲は実務でも必要だったのでちょうどよかったです。</p>
<p>資格試験は目標が立てやすく、ラーニングパスも多く公開されており、学習のとっかかりには非常によいと思うので皆さんもぜひ受けてみてください。</p>
<p>アイキャッチ画像はGoogle Cloud Certificationから付与されたものになります。</p>
]]></content>
    <summary type="html">最近Data Engineeringを扱うプロジェクトへ異動したこともあり、Google CloudにおけるData Engineeringを網羅的に学びたく、Professional Data Engineer認定資格を受けてきました。</summary>
    <category term="DataEngineering" scheme="https://future-architect.github.io/categories/DataEngineering/"/>
    <category term="GoogleCloud" scheme="https://future-architect.github.io/tags/GoogleCloud/"/>
    <category term="PDE" scheme="https://future-architect.github.io/tags/PDE/"/>
    <category term="Udemy" scheme="https://future-architect.github.io/tags/Udemy/"/>
    <category term="合格記" scheme="https://future-architect.github.io/tags/%E5%90%88%E6%A0%BC%E8%A8%98/"/>
  </entry>
  <entry>
    <title>Great ExpectationsでBigQueryのデータ品質を監視する</title>
    <link href="https://future-architect.github.io/articles/20230531a/"/>
    <id>https://future-architect.github.io/articles/20230531a/</id>
    <published>2023-05-30T15:00:00.000Z</published>
    <updated>2023-05-30T15:00:00.000Z</updated>
    <author><name>板野竜也</name></author>
    <content type="html"><![CDATA[<h2 id="1-はじめに">1. はじめに</h2><p>こんにちは、フューチャーでアルバイトをしている板野です。</p>
<p>Great Expectationsというツールを使って、表形式データの品質をバリデーションする流れをご紹介します。</p>
<p>MLOpsを推進するにあたりMLモデルの監視が必要となってきています。その中でも、MLモデルに入出力されるデータ品質をバリデーションすることは重要な監視事項の1つです。</p>
<p>ML監視についての概要や意義については、こちらの記事で詳しく述べられているのでぜひご覧ください。</p>
<h2 id="2-Great-Expectationsの概要">2. Great Expectationsの概要</h2><figure><img fetchpriority="high" src="/images/2023/20230531a/2023-05-10-16-41-00.png" alt="" width="500" height="133"><figcaption>※公式サイトロゴ</figcaption></figure>
<p>Great Expectations（GX）はデータ品質監視ツールの1つで、表形式データの品質監視ができます。GXはOSSであり、Pythonライブラリとして提供されています。</p>
<p>予めデータに対し、Expectationと呼ばれる「データのあるべき姿」を定義しておき、監視対象のデータがこれに逸脱していないかをチェック（バリデーション）します。Expectationは例えば「この列の最大値は100,最小値は50」といったものがあり、GXではExpectationを複数定義することが一般的です。Expectationを複数定義したものをExpectation Suiteと呼びます。</p>
<p>GXでは「監視対象データ、Expectation Suite、バリデーションを実施した後の行動」の3点をまとめたものをCheckpointと呼び、定期的にCheckpointを実行することが、GXにおけるデータ品質監視の一般的な流れとなります。</p>
<h2 id="3-利用の流れ">3. 利用の流れ</h2><h3 id="3-1-事前準備">3.1. 事前準備</h3><h4 id="必要な環境">必要な環境</h4><ul>
<li>Python環境</li>
<li>JupyterNotebook環境（必須ではないですが初期設定ではあった方が楽です）</li>
</ul>
<h4 id="使用するデータ">使用するデータ</h4><p>Bike Sharing Datasetを利用し、1時間毎のシェアバイク利用者数が記録されているテーブルを使います。</p>
<p>以下のような内容になっています。</p>
<img src="/images/2023/20230531a/2023-05-08-16-05-51.png" alt="" width="1200" height="212" loading="lazy">

<p>例えば、<code>hr</code>（時間）の列は0から23までの値しか入らないはずです。もしも、<code>hr</code>が27のような不正値をとる行が入ってきた場合、検知するというのがGXの使いどころです。</p>
<p>今回は<code>hr</code>に27という不正値を故意に入れてみて、これをGXで検知するまでの流れを実践し、以下に説明していきます。</p>
<h3 id="3-2-GXのインストール">3.2. GXのインストール</h3><p><code>pip install great-expectations</code>コマンドでGXをインストールします。</p>
<p>執筆当時のバージョンは<code>0.16.8</code>です。</p>
<figure class="highlight console"><table><tr><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">pip install great-expectations</span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">pip freeze | grep great  <span class="comment"># バージョン確認</span></span></span><br><span class="line">great-expectations==0.16.8</span><br></pre></td></tr></table></figure>

<h3 id="3-3-プロジェクトの作成">3.3. プロジェクトの作成</h3><p><code>great_expectations init</code>コマンドでGXプロジェクトを作成します。</p>
<p>コマンドを実行したディレクトリに<code>great_expectations</code>ディレクトリが自動生成されます。</p>
<figure class="highlight console"><table><tr><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">great_expectations init</span></span><br><span class="line"></span><br><span class="line">  ___              _     ___                  _        _   _</span><br><span class="line"> / __|_ _ ___ __ _| |_  | __|_ ___ __  ___ __| |_ __ _| |_(_)___ _ _  ___</span><br><span class="line">| (_ | &#x27;_/ -_) _` |  _| | _|\ \ / &#x27;_ \/ -_) _|  _/ _` |  _| / _ \ &#x27; \(_-&lt;</span><br><span class="line"> \___|_| \___\__,_|\__| |___/_\_\ .__/\___\__|\__\__,_|\__|_\___/_||_/__/</span><br><span class="line">                                |_|</span><br><span class="line">             ~ Always know what to expect from your data ~</span><br><span class="line"></span><br><span class="line">Let&#x27;s create a new Data Context to hold your project configuration.</span><br><span class="line"></span><br><span class="line">Great Expectations will create a new directory with the following structure:</span><br><span class="line"></span><br><span class="line">    great_expectations</span><br><span class="line">    |-- great_expectations.yml</span><br><span class="line">    |-- expectations</span><br><span class="line">    |-- checkpoints</span><br><span class="line">    |-- plugins</span><br><span class="line">    |-- .gitignore</span><br><span class="line">    |-- uncommitted</span><br><span class="line">        |-- config_variables.yml</span><br><span class="line">        |-- data_docs</span><br><span class="line">        |-- validations</span><br><span class="line"></span><br><span class="line">OK to proceed? [Y/n]: Y</span><br><span class="line"></span><br><span class="line">================================================================================</span><br><span class="line"></span><br><span class="line">Congratulations! You are now ready to customize your Great Expectations configuration.</span><br><span class="line"></span><br><span class="line">You can customize your configuration in many ways. Here are some examples:</span><br><span class="line"></span><br><span class="line">  Use the CLI to:</span><br><span class="line">    - Run `great_expectations datasource new` to connect to your data.</span><br><span class="line">    - Run `great_expectations checkpoint new &lt;checkpoint_name&gt;` to bundle data with Expectation Suite(s) in a Checkpoint for later re-validation.</span><br><span class="line">    - Run `great_expectations suite --help` to create, edit, list, profile Expectation Suites.</span><br><span class="line">    - Run `great_expectations docs --help` to build and manage Data Docs sites.</span><br><span class="line"></span><br><span class="line">  Edit your configuration in great_expectations.yml to:</span><br><span class="line">    - Move Stores to the cloud</span><br><span class="line">    - Add Slack notifications, PagerDuty alerts, etc.</span><br><span class="line">    - Customize your Data Docs</span><br><span class="line"></span><br><span class="line">Please see our documentation for more configuration options!</span><br></pre></td></tr></table></figure>

<p>自動生成されたディレクトリの構成を簡潔に説明すると、以下の通りになります。</p>
<figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">great_expectations  <span class="comment"># GXのルートとなるディレクトリ</span></span><br><span class="line">|-- great_expectations.yml  <span class="comment"># プロジェクト全体の設定ファイル</span></span><br><span class="line">|-- expectations  <span class="comment"># Expectationsを定義したJSONファイルが格納されるディレクトリ</span></span><br><span class="line">|-- checkpoints  <span class="comment"># Checkpointを定義したyamlファイルが格納されるディレクトリ</span></span><br><span class="line">|-- plugins  <span class="comment"># プラグイン用のディレクトリ（本記事では扱わない）</span></span><br><span class="line">|-- .gitignore  <span class="comment"># uncommittedディレクトリをGitにコミットしないように書かれたgitignoreファイル</span></span><br><span class="line">|-- uncommitted  <span class="comment"># Gitで管理する際にコミットされないディレクトリ</span></span><br><span class="line">    |-- config_variables.yml  <span class="comment"># 公開したくないキーや設定が書かれたファイル</span></span><br><span class="line">    |-- data_docs  <span class="comment"># バリデーション結果がHTML等のドキュメントの形式で入ったディレクトリ</span></span><br><span class="line">    |-- validations  <span class="comment"># バリデーション結果のメタデータ(JSON)が入ったディレクトリ</span></span><br></pre></td></tr></table></figure>

<h3 id="3-4-データソースの登録">3.4. データソースの登録</h3><p>次に、監視対象データの場所（データソース）を定義する必要があります。</p>
<p>GXでは、Pandasで扱えるファイルや、SQLベースのクエリで取得できるデータなどに対応しています。今回はBigQueryのテーブルをデータソースとして登録します。</p>
<p><code>great_expectations datasource new</code>コマンドを実行すると、最初にデータソースの種類の選択が促され、自動的にNotebookが起動します。このタイミングでNotebookを起動させたく無ければ<code>--no-jupyter</code>オプションを末尾に付けます。</p>
<figure class="highlight console"><table><tr><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">great_expectations datasource new --no-jupyter</span></span><br><span class="line"></span><br><span class="line">What data would you like Great Expectations to connect to?</span><br><span class="line">    1. Files on a filesystem (for processing with Pandas or Spark)</span><br><span class="line">    2. Relational database (SQL)</span><br><span class="line">: 2</span><br><span class="line"></span><br><span class="line">Which database backend are you using?</span><br><span class="line">    1. MySQL</span><br><span class="line">    2. Postgres</span><br><span class="line">    3. Redshift</span><br><span class="line">    4. Snowflake</span><br><span class="line">    5. BigQuery</span><br><span class="line">    6. Trino</span><br><span class="line">    7. Athena</span><br><span class="line">    8. other - Do you have a working SQLAlchemy connection string?</span><br><span class="line">: 5</span><br><span class="line">Please install the optional dependency &#x27;black&#x27; to enable linting. Returning input with no changes.</span><br><span class="line">To continue editing this Datasource, run jupyter notebook &lt;現在のディレクトリ&gt;/great_expectations/uncommitted/datasource_new.ipynb</span><br></pre></td></tr></table></figure>

<p>コマンドでの対話を進めると、Notebookファイル<code>great_expectations/uncommitted/datasource_new.ipynb</code>が自動的に生成されます。</p>
<p>GXはこのNotebookを実行して、CLIでは設定しずらい詳細な設定を適用していく仕様です。Notebookを使わない場合は直接yamlファイルを編集することになります（補足参照）。</p>
<p>以下の画像はNotebookの冒頭です。</p>
<img src="/images/2023/20230531a/2023-05-08-14-25-51.png" alt="" width="1200" height="707" loading="lazy">

<p>Notebook上の以下の変数を自身のプロジェクトに合うように変更する必要があります。</p>
<figure class="highlight python"><table><tr><td class="code"><pre><span class="line">datasource_name = <span class="string">&quot;&lt;設定したいデータソース名&gt;&quot;</span> <span class="comment">#好みの名前に設定可能</span></span><br><span class="line"></span><br><span class="line">connection_string = <span class="string">&quot;bigquery://&lt;GCPのプロジェクト名&gt;/&lt;BigQueryのデータセット名&gt;&quot;</span></span><br><span class="line"></span><br><span class="line">schema_name = <span class="string">&quot;&quot;</span> <span class="comment"># 入力不要</span></span><br><span class="line">table_name = <span class="string">&quot;&lt;BigQueryのテーブル名&gt;&quot;</span> <span class="comment"># 監視対象データのテーブル</span></span><br></pre></td></tr></table></figure>

<p>上記の変数を変更した後、Notebookのセルを全て実行するとデータソースの設定は完了です。</p>
<h4 id="補足">補足</h4><p><code>datasource_new.ipynb</code>では、GX全体の設定ファイルである<code>great_expectations.yml</code>のデータソースを定義する部分を編集しているだけで、Notebookはこれを編集するための分かりやすいインターフェースに過ぎません。</p>
<p>従って、Notebookを使わずに<code>great_expectations.yml</code>のデータソース定義部分を直接編集するだけで設定が可能です。</p>
<p>例えばBigQueryなら、以下のようにデータソースを定義します（公式Docs参考）</p>
<figure class="highlight yaml"><table><tr><td class="code"><pre><span class="line"><span class="attr">name:</span> <span class="string">my_datasource</span></span><br><span class="line"><span class="attr">class_name:</span> <span class="string">Datasource</span></span><br><span class="line"><span class="attr">execution_engine:</span></span><br><span class="line">  <span class="attr">class_name:</span> <span class="string">SqlAlchemyExecutionEngine</span></span><br><span class="line">  <span class="attr">connection_string:</span> <span class="string">bigquery://&lt;GCPのプロジェクト名&gt;/&lt;BigQueryのデータセット名&gt;</span></span><br><span class="line"><span class="attr">data_connectors:</span></span><br><span class="line">   <span class="attr">default_runtime_data_connector_name:</span></span><br><span class="line">       <span class="attr">class_name:</span> <span class="string">RuntimeDataConnector</span></span><br><span class="line">       <span class="attr">batch_identifiers:</span></span><br><span class="line">           <span class="bullet">-</span> <span class="string">default_identifier_name</span></span><br><span class="line">   <span class="attr">default_inferred_data_connector_name:</span></span><br><span class="line">       <span class="attr">class_name:</span> <span class="string">InferredAssetSqlDataConnector</span></span><br><span class="line">       <span class="attr">include_schema_name:</span> <span class="literal">true</span></span><br></pre></td></tr></table></figure>

<h3 id="3-5-Expectation-Suiteの作成">3.5. Expectation Suiteの作成</h3><p>続いて、Expectation Suiteを作成します。</p>
<p>Expectation Suiteは複数のExpectationの集まりのことを指します。1つ1つ手作業でExpectationを定義・バリデーションしていくのは非効率なため、Expectation Suiteを定義してまとめて行うのです。</p>
<p><code>great_expectations suite new</code>コマンドを実行すると、先程と似た流れでCLIとNotebookを使ってセットアップを行います。最初の「How would you like to create your Expectation Suite?」という質問に「3」と回答するとExpectation Suiteを自動で生成してくれます。</p>
<p>今回はExpectation Suiteを自動生成してもらいます。</p>
<figure class="highlight console"><table><tr><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">great_expectations suite new --no-jupyter</span></span><br><span class="line"></span><br><span class="line">How would you like to create your Expectation Suite?</span><br><span class="line">    1. Manually, without interacting with a sample Batch of data (default)</span><br><span class="line">    2. Interactively, with a sample Batch of data</span><br><span class="line">    3. Automatically, using a Data Assistant</span><br><span class="line">: 3</span><br><span class="line"></span><br><span class="line">A batch of data is required to edit the suite - let&#x27;s help you to specify it.</span><br><span class="line"></span><br><span class="line">Select data_connector</span><br><span class="line">    1. default_runtime_data_connector_name</span><br><span class="line">    2. default_inferred_data_connector_name</span><br><span class="line">    3. default_configured_data_connector_name</span><br><span class="line">: 3</span><br><span class="line"></span><br><span class="line">Which data asset (accessible by data connector &quot;default_configured_data_connector_name&quot;) would you like to use?</span><br><span class="line">    1. &lt;テーブル名&gt;</span><br><span class="line"></span><br><span class="line">Type [n] to see the next page or [p] for the previous. When you&#x27;re ready to select an asset, enter the index.</span><br><span class="line">: 1</span><br><span class="line"></span><br><span class="line">Name the new Expectation Suite [&lt;テーブル名&gt;.warning]: exp_suite_test</span><br><span class="line"></span><br><span class="line">Great Expectations will create a notebook, containing code cells that select from available columns in your dataset and</span><br><span class="line">generate expectations about them to demonstrate some examples of assertions you can make about your data.</span><br><span class="line"></span><br><span class="line">When you run this notebook, Great Expectations will store these expectations in a new Expectation Suite &quot;exp_suite_test&quot; here:</span><br><span class="line"></span><br><span class="line">  file:///&lt;現在のディレクトリ&gt;/great_expectations/expectations/exp_suite_test.json</span><br><span class="line"></span><br><span class="line">Would you like to proceed? [Y/n]: Y</span><br><span class="line"><span class="meta prompt_"></span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">中略</span></span><br><span class="line"></span><br><span class="line">To continue editing this suite, run jupyter notebook &lt;現在のディレクトリ&gt;/great_expectations/uncommitted/edit_exp_suite_test.ipynb</span><br></pre></td></tr></table></figure>

<p>コマンドの実行が完了すると、Expectation Suiteを設定するためのNotebookファイル<code>great_expectations/uncommitted/edit_exp_suite_test.ipynb</code>が自動生成されます。</p>
<p>以下の画像はNotebookの冒頭です。</p>
<img src="/images/2023/20230531a/2023-05-08-15-59-41.png" alt="" width="1200" height="1107" loading="lazy">

<p>Notebookにて、必要に応じて変更すべき変数は以下の2つです。</p>
<ul>
<li>batch_request<ul>
<li>‘limit’ の数はデフォルトで<code>1000</code>となっていますが、必要に応じて変更します</li>
<li>この値は一度のバリデーションでBigQueryのテーブルデータを何件読み込んでくるかの数値です</li>
<li>‘limit’ の数値が大きすぎると処理が重くなる可能性があります</li>
</ul>
</li>
<li>exclude_column_names<ul>
<li><strong>バリデーションしたい列</strong>をコメントアウトします</li>
<li>初期状態のままでは全てがexcludeされている状態なので、どの列もバリデーションしないというおかしな設定になってしまいます</li>
<li>今回は<code>hr</code>列だけバリデーションしたいのでここだけコメントアウトしています</li>
</ul>
</li>
</ul>
<p>Notebookのセルを全て実行すると、自動でExpectation Suiteが作成され、そのExpectation Suiteが定義されたJSONファイルが<code>great_expectations/expectations/</code>配下に保存されます。</p>
<p>以下はそのJSONファイルを一部展開して表示した画像です。</p>
<img src="/images/2023/20230531a/2023-05-08-16-22-06.png" alt="" width="715" height="853" loading="lazy">

<p>expectationは計13個自動生成されたようです。</p>
<p>その中の1つは<code>expect_column_values_to_be_between</code>というもので、「<code>hr</code>列は0から23までの値をとるはずである」という内容のexpectationです。</p>
<p>またこの時点で、Expectation Suiteの生成と同時に、データのバリデーションまで行われています。</p>
<p><code>great_expectations/uncommitted/data_docs/local_site/index.html</code>を開くと以下のような画面があり、1度バリデーションが行われていることが分かります。</p>
<img src="/images/2023/20230531a/2023-05-10-10-18-29.png" alt="" width="1200" height="303" loading="lazy">

<p>クリックして詳細を見てみると、2つのExpectationに不合格となっているようです。<br>自動生成のExpectationが何個も定義されているので、多少は変なExpectationが生成されることもあるのでしょう。</p>
<img src="/images/2023/20230531a/2023-05-10-10-20-31.png" alt="" width="1200" height="990" loading="lazy">

<h3 id="3-6-Checkpointの作成・実行">3.6. Checkpointの作成・実行</h3><p>最後に、Checkpointを作成する必要があります。</p>
<p>Checkpointとは「監視対象データ（データソース）、Expectation Suite、バリデーションを実施した後の行動」の3点をまとめたものであり、Checkpointを実行することで、Expectation Suiteをまとめてバリデーションできます。</p>
<p>バリデーションを実施した後の行動として、結果をメールやSlackでの通知する等が挙げられますが、Pythonでプログラミングできるものなら何でも可能となっており、自由度が高いです（公式Docs参考）<br>※今回はバリデーションを実施した後の行動の設定までは扱いません。</p>
<p><code>great_expectations checkpoint new &lt;設定したいcheckpoint名&gt;</code>コマンドを実行すると、<code>great_expectations/uncommitted/edit_checkpoint_~~.ipynb</code>にNotebookファイルが自動生成されます。</p>
<figure class="highlight console"><table><tr><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">great_expectations checkpoint new --no-jupyter checkpoint_test</span></span><br><span class="line"></span><br><span class="line">Please install the optional dependency &#x27;black&#x27; to enable linting. Returning input with no changes.</span><br><span class="line"><span class="meta prompt_"></span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">中略</span></span><br><span class="line"></span><br><span class="line">To continue editing this Checkpoint, run jupyter notebook &lt;現在のディレクトリ&gt;/great_expectations/uncommitted/edit_checkpoint_checkpoint_test.ipynb</span><br></pre></td></tr></table></figure>

<p>生成されたNotebookファイルで変更する必要がある部分は次の通りです。</p>
<p>初期状態では<code>data_asset_name</code>の行がBigQuery上の適当なテーブルになっているので、監視対象にしたいデータセット名、テーブル名に書き換えます。</p>
<figure class="highlight python"><table><tr><td class="code"><pre><span class="line">my_checkpoint_name = <span class="string">&quot;checkpoint_test&quot;</span> <span class="comment"># This was populated from your CLI command.</span></span><br><span class="line"></span><br><span class="line">yaml_config = <span class="string">f&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">name: <span class="subst">&#123;my_checkpoint_name&#125;</span></span></span><br><span class="line"><span class="string">config_version: 1.0</span></span><br><span class="line"><span class="string">class_name: SimpleCheckpoint</span></span><br><span class="line"><span class="string">run_name_template: &quot;%Y%m%d-%H%M%S-my-run-name-template&quot;</span></span><br><span class="line"><span class="string">validations:</span></span><br><span class="line"><span class="string">  - batch_request:</span></span><br><span class="line"><span class="string">      datasource_name: my_datasource</span></span><br><span class="line"><span class="string">      data_connector_name: default_inferred_data_connector_name</span></span><br><span class="line"><span class="string">      data_asset_name: &lt;監視したいデータセット名&gt;.&lt;監視したいテーブル名&gt; #★ここを変更する</span></span><br><span class="line"><span class="string">      data_connector_query:</span></span><br><span class="line"><span class="string">        index: -1</span></span><br><span class="line"><span class="string">    expectation_suite_name: my_exp_suite</span></span><br><span class="line"><span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="built_in">print</span>(yaml_config)</span><br></pre></td></tr></table></figure>

<p>今回はExpectation Suiteの自動生成に用いたテーブルに「<code>hr</code>（時間）の列の値を27に変更した不正な行」を追加したテーブルを監視対象としてCheckpointを作成しました。</p>
<p>Notebookの全てのセルを実行し、末尾のセルのコメントアウトを外して実行すると、Checkpointが実行されます。Checkpointの実行結果は先程同様に<code>great_expectations/uncommitted/data_docs/local_site/index.html</code>を開いて閲覧できます。</p>
<p>以下のように、不正な行を1行追加しただけで不合格の項目が増えていることが確認できます。</p>
<p>このようにしてデータの不正・品質劣化を監視できます。</p>
<img src="/images/2023/20230531a/2023-05-10-10-45-19.png" alt="" width="1200" height="923" loading="lazy">

<p>以上でCheckpointを実行するまでの流れは終了です。</p>
<p>上記（3.1.~3.5.）の手順を実行しておけば、今後はCheckpointを実行するだけでバリデーションできます。</p>
<p>Checkpointの定義はyamlファイルとして保存されており、PythonまたはCLIからAPIを呼び出すだけで何度でも実行できます。</p>
<h2 id="4-Tips">4. Tips</h2><p>以下はGXの調査検証を進めていくにあたり生じた疑問とその答えをまとめたものです。</p>
<p>GXの利用を検討しているさいはご参考ください。</p>
<h3 id="Expectation-Suiteを編集したいときは？">Expectation Suiteを編集したいときは？</h3><p><code>great_expectations suite edit &lt;編集したいExpectation Suite名&gt;</code>コマンドにより編集できます。</p>
<p>CLIコマンドによる対話形式で「2. Interactively, with a sample batch of data」の選択肢を選ぶと、Notebook形式のインターフェースでExpectationを一つ一つ編集できます。</p>
<figure class="highlight console"><table><tr><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">great_expectations suite edit exp_suite_test --no-jupyter</span></span><br><span class="line"></span><br><span class="line">How would you like to edit your Expectation Suite?</span><br><span class="line">    1. Manually, without interacting with a sample batch of data (default)</span><br><span class="line">    2. Interactively, with a sample batch of data</span><br><span class="line">: 2</span><br><span class="line"></span><br><span class="line">A batch of data is required to edit the suite - let&#x27;s help you to specify it.</span><br><span class="line"></span><br><span class="line">Select data_connector</span><br><span class="line">    1. default_runtime_data_connector_name</span><br><span class="line">    2. default_inferred_data_connector_name</span><br><span class="line">    3. default_configured_data_connector_name</span><br><span class="line">: 3</span><br><span class="line"></span><br><span class="line">Which data asset (accessible by data connector &quot;default_configured_data_connector_name&quot;) would you like to use?</span><br><span class="line">    1. &lt;テーブル名&gt;</span><br><span class="line"></span><br><span class="line">Type [n] to see the next page or [p] for the previous. When you&#x27;re ready to select an asset, enter the index.</span><br><span class="line">: 1</span><br><span class="line"><span class="meta prompt_"></span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">中略</span></span><br><span class="line"></span><br><span class="line">To continue editing this suite, run jupyter notebook &lt;現在のディレクトリ&gt;/great_expectations/uncommitted/edit_exp_suite_test.ipynb</span><br></pre></td></tr></table></figure>

<p>JSONファイルの直接編集もできますが、複雑なため、Notebook形式やPythonのAPI経由で編集することをお勧めします（公式Docs参考）</p>
<h3 id="GCPにおける構成例は？">GCPにおける構成例は？</h3><p>公式Docsによると、GCPを利用する場合、以下のような構成で動かす一例が挙げられています。</p>
<p>設定のための初回実行はローカル環境で行い、定期実行する際はCloud Composerを利用します。</p>
<p>また、メタデータやバリデーション結果のドキュメント等はGCSに保存しておきます。</p>
<p>バリデーション結果のドキュメントはHTML形式なのでGCSのエンドポイントにアクセスして閲覧できる設定をすれば便利そうです。</p>
<img src="/images/2023/20230531a/2023-05-10-15-59-58.png" alt="" width="1200" height="903" loading="lazy">

<p>※Great Expectations 公式Docsより画像引用</p>
<h3 id="Expectationにはどんな種類があるか？">Expectationにはどんな種類があるか？</h3><p>多すぎて把握出来ていませんが、公式コミュニティに既存のExpectationsが300個以上あります。</p>
<p>Expectationの自作も可能であり、カスタマイズ性は非常に高いです（公式Docs参考）</p>
<h2 id="5-おわりに">5. おわりに</h2><p>今回は、Great Expectations（GX）を利用してBigQueryのデータ品質を監視する簡単な流れを紹介をしました。</p>
<p>GXには様々な機能や拡張性を備えており、様々なユースケースにカスタマイズできます。</p>
<p>本記事が読者の皆様のご参考になれば幸いです。</p>
]]></content>
    <summary type="html">Great Expectationsというツールを使って、表形式データの品質をバリデーションする流れをご紹介します。MLOpsを推進するにあたりMLモデルの監視が必要となってきています。その中でも...</summary>
    <category term="DataEngineering" scheme="https://future-architect.github.io/categories/DataEngineering/"/>
    <category term="AI監視" scheme="https://future-architect.github.io/tags/AI%E7%9B%A3%E8%A6%96/"/>
    <category term="BigQuery" scheme="https://future-architect.github.io/tags/BigQuery/"/>
    <category term="Great Expectations" scheme="https://future-architect.github.io/tags/Great-Expectations/"/>
    <category term="データマネジメント" scheme="https://future-architect.github.io/tags/%E3%83%87%E3%83%BC%E3%82%BF%E3%83%9E%E3%83%8D%E3%82%B8%E3%83%A1%E3%83%B3%E3%83%88/"/>
    <category term="バリデーション" scheme="https://future-architect.github.io/tags/%E3%83%90%E3%83%AA%E3%83%87%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3/"/>
  </entry>
  <entry>
    <title>書籍紹介：大規模データ管理(エンタープライズアーキテクチャのベストプラクティス)</title>
    <link href="https://future-architect.github.io/articles/20230529a/"/>
    <id>https://future-architect.github.io/articles/20230529a/</id>
    <published>2023-05-28T15:00:00.000Z</published>
    <updated>2023-05-28T15:00:00.000Z</updated>
    <author><name>杉江伸祐</name></author>
    <content type="html"><![CDATA[<p>最近読んだ書籍の中で非常に良質な内容でしたので紹介したいと思います。少しでも多くの方に興味を持ってもらえることを期待しています。</p>
<img fetchpriority="high" src="/images/2023/20230529a/book_datamanagement_at_scale.jpeg" alt="" width="516" height="660">

<p>O’Reilly Japan</p>
<h2 id="はじめに">はじめに</h2><p>私自身がデータ管理（データマネジメント）という観点でここ数年様々な検討をしてきていますので前提としてその背景について簡単にまとめてみます。</p>
<p>かつてオンプレミスで運用を行っていた時は企業内のデータは完全に管理されていました。データウェアハウスを導入してデータの集約・加工は行われていましたが、専門チームがデータ仕様確認やデータ提供までもすべての責任を担っていました。品質は高いのですが利用者からの要望（新しいデータの提供、仕様の変更）の対応についてはスピード大きな制約がありました。また大規模なデータを扱うためには多大なコストが必要という制約もあります。</p>
<p>クラウド技術による「スモールスタートを可能とするインフラ」「大規模なデータを扱うための適切な技術要素」を利用して一気にクラウドアーキテクチャの利用が増えました。クラウドベンダーからの積極的な情報展開もあってデータ基盤としてデータレイクアーキテクチャを利用するという考えが普及した感があります。データレイクではデータを集中して管理するのではなく、利用者が自分たちで自由に安全に利用できるための基盤を整備するという考え方です。</p>
<p>これによりDX化のスピード要求に答えることができてめでたしと思いたいところですが、発展的、継続的に利用ができるのかという点については課題があります。多くの場合「生データそのまま配置しておくので利用者（システム）が自由に使ってください」というアプローチですが、それだけだと「誰（チーム）がそのデータに責任を持つのか」というものが不明確になりがちです。ここがあいまいだとビジネスの変化（データの変化）が発生した際に、データ利用者が追従していくことが難しく足かせになることが容易に想像できます。関係者が少人数であれば密なコミュニケーションが可能なため「データ集めておいたのでご自由にどうぞ」というのは成立しますが、関係者が増えていくとそれは難しくなります。</p>
<p>クラウド前、クラウド後という環境の変化に関するこれらの背景は本書の1章「データ管理の崩壊」でも触れられています。この点の問題意識は私の感覚に非常に近いものがあります。</p>
<p>本書では共通基盤としてのデータレイクプロジェクトの6割が失敗してしまうという言葉が引用されていますが、あながち間違いでもないかもしれません。</p>
<h2 id="エンタープライズアーキテクチャのベストプラクティス">エンタープライズアーキテクチャのベストプラクティス</h2><p>本書のサブタイトルである「エンタープライズアーキテクチャのベストプラクティス」で目指すものは一言でいうと以下です。</p>
<blockquote>
<p>すべてのデータを1つのサイロに集めるのではなく、ユーザーが自分たちで簡単かつ安全にデータを流通、利用、活用できるような方法に移行する</p>
</blockquote>
<p>これを実現するためのデータ管理についてのフレーム（知識体系）については、DAMA(Data Management Association) によって、DMBOK(Data Management Body of Knowledge)があります。DAMAホイール図として11個の知識体系が示されていてその図は見たことがある方も多いのではないかと思います。</p>
<img src="/images/2023/20230529a/DMBOK2_Wheel.PNG" alt="DMBOK2_Wheel.PNG" width="383" height="403" loading="lazy">

<p>The Global Data Management Community</p>
<p>本書もそのフレームは活用して議論が展開されていますが、DAMAホイール図の項目についてなぞっているだけの内容にはなっていません。DMBOKについてはデータ基盤を整備するエンジニアにとってはとらえどころのない話が多く、理解が難しいのが実情ではないかと思われます。ついついモノづくりが先行してしまい今見えている課題（やりたいこと）だけの個別最適化となる例が多いように思われます。</p>
<p>本書では、「データ統合と相互運用性(Data Integration &amp; Interoperability)」にかなりの重点（むしろそこがメインとして）をおいて記載されています。</p>
<ul>
<li>データ統合<br>3つのアーキテクチャの活用について紹介されています。<ul>
<li>RDS(Read-Only Data Store)アーキテクチャ</li>
<li>APIアーキテクチャ</li>
<li>ストリーミングアーキテクチャ</li>
</ul>
</li>
<li>相互運用性<br>データ提供側からデータ利用側の一連のフローの中でどこを責任範疇とするのかという責任分界点についての解説</li>
</ul>
<p>データ統合はクラウドアーキテクチャをベースとした概要の解説となります。論理モデルの解説で具体的なプロダクトについては本書では深く触れられていません。こちらは様々な資料もネット上で公開されているのでデータ連携に携わった人であれば改めて確認する必要はないかもしれません。ここでの紹介は省略します。</p>
<h2 id="データの相互運用性">データの相互運用性</h2><p>本書の一番の読みどころは2つ目の「相互運用性」についての解説であると考えます。「誰がそのデータに責任を持つのか」というデータの責任範囲（境界）についての考察です。</p>
<p>本書で紹介されている概念図を1枚だけ引用させてもらいます（少し改変しています）。<br>データレイヤはデータを流通する要素となる、RDS(Read-Only Data Store)、API基盤、メッセージング基盤、その組み合わせを包含するもので、論理的なものを表しています。</p>
<img src="/images/2023/20230529a/data_Interoperability.PNG" alt="data_Interoperability.PNG" width="585" height="335" loading="lazy">

<p>ここでのポイントは2つです。</p>
<ul>
<li>データの流通はデータレイヤを通じて行われる</li>
<li>データ提供チーム（すなわちオリジナルデータの所有者）は データレイヤを通じてデータ利用側に渡すところまで責任を持つ</li>
</ul>
<p>本書はこれらを基本原則として、一貫性をもってまとめられています。</p>
<p>この考えはある意味当たり前という印象を受けるかもしれませんが、現実の場面であいまいになりがちな「データはだれが管理すべきか」という点を重視してアーキテクチャ設計の基本原則に組み入れる形で紹介してくれている点が本書の優れた点であると考えます。</p>
<p>本書ではDDD（ドメイン駆動設計）の概念を引用して説明しています。アプリケーションの境界（ドメイン）を明確に（強制的に）定義するのと同じく、データについても責任範囲を明確化してガバナンスの基本方針とします。具体的はデータ提供チームがデータ利用チームが利用しやすいようにデータを提供するところまでが責任範囲として定義するという考え方です。データを提供する手段（アーキテクチャ）はデータ要件により決定すれば良いのですが、提供内容（利用しやすいデータモデル、データ仕様）についても責任を持つことになります（責任を持つのはデータ基盤の整備チームでもデータを利用するチームではないということです）。データレイヤを通じてデータを流通させますが、データレイヤの具体的な実装は責任範囲に従い厳密に分離します。</p>
<p>データ基盤におけるアーキテクチャ設計をするにあたってデータマネジメント（ データの相互運用性）方針が重要であるということが腹に落ちるのではないかと思います。現実の場面においては、個別の事情や実態に合わせた考え方が当然必要にはなるのですが、方針検討において参考になるはずです。</p>
<p>しみじみとする文を引用しておきます。</p>
<ul>
<li>「このような中央機能を構築するためには、個々のチームが統合パターンやツールに関する決定権を放棄する必要があります。これには抵抗があるでしょう。社内政治的な選択が必要になるかもしれません」</li>
<li>「このようなデータランドスケープの近代化には、現実的なアプローチが必要です。なぜなら、密結合されたランドスケープからの移行は非常に難しいからです。単純なデータフローから小さく始めて、徐々に拡大していくことでドメインやユーザーはメリットを意識し、組織の競争力を高める新しいアーキテクチャに貢献したいと考えるようになります」</li>
<li>「データサービスを自分たちで導入し、データサイロを他のサービスで置き換えるだけのチームは「データスプロール（補足：無計画なデータ量とその種類の広がり）」のリスクを抱えるようになります」</li>
<li>「スケールアップのためには絶え間ないコミュニケーション、貢献、そして強力なデータガバナンスが必要です」</li>
</ul>
<h2 id="さいごに">さいごに</h2><p>本書はタイトルの「大規模データ管理(エンタープライズアーキテクチャのベストプラクティス)」あるように、システムアーキテクチャではなく、エンタープライズ領域のデータマネジメントについて本質的な話をまとめてくれている良書です。</p>
<p>以下のようなDAMAホイール図にあるデータマネジメントの一通りについても（エンジニア目線で）紹介されています。</p>
<ul>
<li>データガバナンス</li>
<li>データセキュリティ</li>
<li>データの価値化（データ利用）</li>
<li>マスタデータ管理(MDM)</li>
<li>メタデータの活用</li>
</ul>
<p>データマネジメントについて自分のかかわっているプロジェクトにあてはめて思考してみるのも有意義ではないかと思います。ただしあくまでも参考文献なので自分自身での解釈が必要であるという点は忘れなく。</p>
]]></content>
    <summary type="html">最近読んだ書籍の中で非常に良質な内容でしたので紹介したいと思います。少しでも多くの方に興味を持ってもらえることを期待しています。</summary>
    <category term="DataEngineering" scheme="https://future-architect.github.io/categories/DataEngineering/"/>
    <category term="DDD" scheme="https://future-architect.github.io/tags/DDD/"/>
    <category term="DMBOK" scheme="https://future-architect.github.io/tags/DMBOK/"/>
    <category term="アーキテクチャ" scheme="https://future-architect.github.io/tags/%E3%82%A2%E3%83%BC%E3%82%AD%E3%83%86%E3%82%AF%E3%83%81%E3%83%A3/"/>
    <category term="データマネジメント" scheme="https://future-architect.github.io/tags/%E3%83%87%E3%83%BC%E3%82%BF%E3%83%9E%E3%83%8D%E3%82%B8%E3%83%A1%E3%83%B3%E3%83%88/"/>
    <category term="書評" scheme="https://future-architect.github.io/tags/%E6%9B%B8%E8%A9%95/"/>
  </entry>
  <entry>
    <title>Cloud Data Fusionで始めるETL入門</title>
    <link href="https://future-architect.github.io/articles/20230420a/"/>
    <id>https://future-architect.github.io/articles/20230420a/</id>
    <published>2023-04-19T15:00:00.000Z</published>
    <updated>2023-04-19T15:00:00.000Z</updated>
    <author><name>市川浩暉</name></author>
    <content type="html"><![CDATA[<img fetchpriority="high" src="/images/2023/20230420a/image.png" alt="" width="220" height="220">

<h2 id="はじめに">はじめに</h2><p>TIG (Technology Innovation Group)の市川です。<br>春の入門祭り4日目の記事です。</p>
<p>本日の入門記事は「Cloud Data Fusionで始めるETL入門」ということで、Google CloudでETL&#x2F;ELTを構築できる「Cloud Data Fusion」を利用して、ETLを作成します。</p>
<h2 id="ETLとは">ETLとは</h2><p>そもそもETLとはなんでしょうか。</p>
<p>ETLはExtract Transform Loadの略で、データ分析のプロセスの中で重要な役割を果たしています。</p>
<p>データ分析は一般的には、「収集」「加工」「蓄積」「利活用」の順序で行われます。</p>
<div class="scroll"><table>
<thead>
<tr>
<th align="left">プロセス</th>
<th align="left">内容</th>
<th align="left">利用する主なサービス</th>
</tr>
</thead>
<tbody><tr>
<td align="left">収集</td>
<td align="left">企業が保有するさまざまなデータソース(主にRAWデータ)をデータレイクに格納</td>
<td align="left">Cloud Stoarge, S3 等</td>
</tr>
<tr>
<td align="left">加工</td>
<td align="left">データレイクに格納されているデータのクレンジング、整形、および変換を行い、ビジネス上有用な情報を得るためのデータを生成する</td>
<td align="left">Cloud Data Fusion, Dataflow 等</td>
</tr>
<tr>
<td align="left">蓄積</td>
<td align="left">加工されたデータをデータウェアハウス（DWH）に格納する</td>
<td align="left">BigQuery, Redshift 等</td>
</tr>
<tr>
<td align="left">利活用</td>
<td align="left">DWHのデータを利用してBIから参照したり、マーケティング等に利用する。</td>
<td align="left">各種MAツール</td>
</tr>
</tbody></table></div>
<p>この流れの中で、ETLを利用するのは「加工」のフェーズです。</p>
<p>「収集」フェーズでデータレイクに格納されたRAWデータを、データウェアハウスにETLでに取り込みやすいようデータを加工します。</p>
<h2 id="Cloud-Data-Fusionとは何か">Cloud Data Fusionとは何か</h2><p>Cloud Data FusionはGoogle Cloudが提供しているETL&#x2F;ELTを行うためのサービスです。</p>
<p>特徴は以下の通りです。</p>
<ul>
<li>OSSのCDAPを使って構築されたサーバーレスかつフルマネージドなサービスを提供</li>
<li>視覚的に操作できるGUIでエンジニアでなくてもデータ加工パイプラインが作成できる</li>
<li>プラグインが豊富に用意されていて、拡張性が高い</li>
</ul>
<p>Google Cloudでは、ETLツールとしてDataflowやDataprep、Dataform等多くのサービスが提供されていますが、Dataflowは基本的にコーディングが必要であるためエンジニアがいないと構築が難しかったりします。</p>
<p>Cloud Data FusionはGUIベースでETL&#x2F;ELT処理を作成できるため、普段開発作業を担当しないような方にもとっつきやすく、プラグインが豊富なので拡張性が高いのが他のサービスと異なる特徴です。</p>
<h2 id="簡単なパイプラインを作ってみよう">簡単なパイプラインを作ってみよう</h2><p>それでは、早速簡単なパイプラインを作ってみましょう。</p>
<h3 id="セットアップ">セットアップ</h3><p>セットアップ方法はGoogle Cloudのドキュメントをご参照ください。<br>https://cloud.google.com/data-fusion/docs/how-to/create-instance?hl=ja</p>
<h4 id="注意点">注意点</h4><p>CLoud Data Fusionは少しデプロイに時間がかかり、大体立ち上がるまでに20分~30分ほどかかります。<br>また、Cloud Data Fusionでは3つのエディションが展開されていますが、使用感を試してみたい程度であれば月120時間の無料枠が用意されている「Basic」を選ぶと費用が抑えられると思います（Developer　Editionには無料枠が存在しないため）</p>
<p>参考：https://cloud.google.com/data-fusion/pricing?hl=ja</p>
<p>以下のようにインスタンスが立ち上がったら準備完了です。</p>
<img src="/images/2023/20230420a/image_2.png" alt="" width="1057" height="212" loading="lazy">

<h3 id="パイプラインを作ってみる">パイプラインを作ってみる</h3><p>今回は入門編ということで、すでに用意されているパイプラインを利用したいと思います。<br>コンソール画面から「インスタンスを表示」をクリックし、Cloud Data Fusionの画面にアクセスし、ヘッダーの「HUB」をクリックします。</p>
<img src="/images/2023/20230420a/image_3.png" alt="" width="1200" height="945" loading="lazy">

<p>その後、「Pipelines」の「Cloud Data Fusion Quickstart」をクリックし、</p>
<img src="/images/2023/20230420a/image_4.png" alt="" width="1200" height="691" loading="lazy">

<p>Finishを押下して、パイプラインを作成します。</p>
<img src="/images/2023/20230420a/image_5.png" alt="" width="1200" height="773" loading="lazy">

<p>すると、以下のような画面にアクセスできるようになります。</p>
<p>この画面はPipelineのStudioの画面で、GUI形式でパイプラインを作成&#x2F;編集できます。</p>
<img src="/images/2023/20230420a/image_6.png" alt="" width="1200" height="874" loading="lazy">

<p>Studio画面の構成を大きく3つに分けて説明します。</p>
<h3 id="（1）ノードに使うプラグインを選択">（1）ノードに使うプラグインを選択</h3><p>サイドバーにはData Fusionで利用できるプラグインを選択し、利用できます。<br>プラグインは大きく分けて、以下のような種類が用意されています。</p>
<ul>
<li>Source：　起点となるデータソースを指定するプラグイン</li>
<li>Transform・Analytics：各種データを変換するプラグイン</li>
<li>Sink：データの流れの終端として、データの格納先を指定するプラグイン</li>
<li>Conditions and Actions：データ変換とは関係ない何らかのアクション（ファイル移動や削除 等）を行えるプラグイン</li>
<li>Error Handlers and Alerts：エラーハンドリングを行うプラグイン</li>
</ul>
<h3 id="（2）Studio">（2）Studio</h3><p>メインとなるStudioでパイプラインを組み立てます。</p>
<ul>
<li>パイプラインの編集エリア。</li>
<li>１つ１つの箱が、データソースや変換処理、データの格納先を示す”ノード”であり、実行順に線で繋がっている。</li>
<li>ノードは上述したプラグインの種別ごとに色分けされている</li>
</ul>
<h3 id="（3）各種設定">（3）各種設定</h3><p>ここでは、作成したパイプラインの設定やデプロイを行うことができます。</p>
<ul>
<li>パイプライン定義をJSON形式でImport＆Export</li>
<li>プレビュー（テスト）の実行メニューを開く</li>
<li>編集中のパイプラインをDraftとして保存</li>
<li>Draftのパイプラインをデプロイ</li>
</ul>
<h2 id="各ノードの処理内容">各ノードの処理内容</h2><h3 id="GCSからファイルを読み込み">GCSからファイルを読み込み</h3><img src="/images/2023/20230420a/image_7.png" alt="" width="1200" height="573" loading="lazy">

<p>各ノードの設定内容を確認する際は、マウスオーバーした際に表示される「Properties」をクリックして内容を確認します。</p>
<p>GCS Propertiesをクリックすると以下のような画面が表示されます。</p>
<p>以下のパス」部分でGCSのオブジェクトを指定しています。<br>（デフォルトで設定されているファイルはサンプルファイルとして公開されているため、動かす上で変更の必要はありません）</p>
<img src="/images/2023/20230420a/image_8.png" alt="" width="1200" height="930" loading="lazy">

<h3 id="データ加工">データ加工</h3><p>データ加工は主に、Wranglerと呼ばれるプラグインで行います。</p>
<p>Wranglerでは、記載されたDirectivesを元にデータを加工します。<br>（個人的にこれがCloud Data Fusionの一番便利な機能だと思っています）。</p>
<img src="/images/2023/20230420a/image_9.png" alt="" width="1200" height="559" loading="lazy">

<p>Directivesを作成する際は、以下のような形でクリックをするだけでファイルの加工（Parse処理やデータ型の変換等）を行うことができます。</p>
<p>以下のようなファイルをWranglerで読み込んだ際に、</p>
<img src="/images/2023/20230420a/image_10.png" alt="" width="1200" height="850" loading="lazy">

<p>次の画像のように操作することで、</p>
<img src="/images/2023/20230420a/image_11.png" alt="" width="1200" height="655" loading="lazy">

<p>データ加工した際のイメージと、先ほど定義されたDirectivesが画面上に作成されます。</p>
<img src="/images/2023/20230420a/image_12.png" alt="" width="1200" height="503" loading="lazy">

<p>このように、Wranglerを利用することで、実際にどのようなデータ加工が行われるかをイメージしながら、簡単にETLの処理を作成できます。</p>
<h3 id="BigQueryへのインサート">BigQueryへのインサート</h3><p>加工したデータは最終的にデータウェアハウスであるBigQueryにInsertされます。</p>
<p>サンプルでは、「GCPQuickStart」というデータセットの「top_rated_inexpensive」というテーブルにデータがInsertされるような設定になっています。</p>
<p>今回はデータセット&#x2F;テーブルが既存の環境に存在しない場合に新規作成されるような形になっているので、別途作成する対応は不要です。</p>
<img src="/images/2023/20230420a/image_13.png" alt="" width="1200" height="952" loading="lazy">

<h2 id="デプロイ">デプロイ</h2><p>今回は入門編ということもあるので設定値はデフォルトのままで、<br>以下画像の「Deploy」を押下し、パイプラインをデプロイします。</p>
<img src="/images/2023/20230420a/image_14.png" alt="" width="1200" height="542" loading="lazy">

<h2 id="実行してみる">実行してみる</h2><p>デプロイしたパイプラインを実行してみましょう。<br>以下画像で差している「Run」を押下する事で、パイプラインが実行できます。</p>
<img src="/images/2023/20230420a/image_15.png" alt="" width="1200" height="596" loading="lazy">

<h2 id="BigQueryにInsertされた結果を確認する">BigQueryにInsertされた結果を確認する</h2><p>パイプラインのStatusが「Succeed」になったら処理は成功です。<br>Cloud Data Fusionを作成したプロジェクトのBigQueryの画面にアクセスしてみましょう。</p>
<p>すると、BigQuery側でデータがInsertされていることが確認できました。</p>
<img src="/images/2023/20230420a/image_16.png" alt="" width="1200" height="651" loading="lazy">

<h3 id="開発する際のちょっとしたTips">開発する際のちょっとしたTips</h3><p>Cloud Data Fusionの画面から実行した際のログを確認したい時があります。<br>その際に、「Logs」という部分をクリックしても一部のログのみしか確認できないため、<br>少し見にくいのですが、以下画像の「View Raw Logs」をクリックすると、ログの全量を確認できます。</p>
<img src="/images/2023/20230420a/image_17.png" alt="" width="1200" height="471" loading="lazy">

<h2 id="さいごに">さいごに</h2><p>今回はCloud Data Fusionを利用して、簡単に使用感を確認してみました。</p>
<p>ETL&#x2F;ELTの作成を非エンジニアが担当する場合、GUI形式でパイプラインが組めるので、そういった際には採用候補になりうるサービスだと感じました。</p>
<p>明日は永井優斗さんの、「技術書」の読書術 読書感想文 です。</p>
]]></content>
    <summary type="html">「Cloud Data Fusionで始めるETL入門」ということで、Google CloudでETL/ELTを構築できる「Cloud Data Fusion」を利用して、ETLを作成します。</summary>
    <category term="DataEngineering" scheme="https://future-architect.github.io/categories/DataEngineering/"/>
    <category term="ETL" scheme="https://future-architect.github.io/tags/ETL/"/>
    <category term="GoogleCloud" scheme="https://future-architect.github.io/tags/GoogleCloud/"/>
  </entry>
  <entry>
    <title>Dataflow後編（Dataflowの事前準備からPub/Sub・BigQueryとの連携例まで）</title>
    <link href="https://future-architect.github.io/articles/20220920b/"/>
    <id>https://future-architect.github.io/articles/20220920b/</id>
    <published>2022-09-19T15:00:01.000Z</published>
    <updated>2022-09-19T15:00:01.000Z</updated>
    <author><name>平野甫</name></author>
    <content type="html"><![CDATA[<img fetchpriority="high" src="/images/2022/20220920b/dataflow_top2.png" alt="" width="1000" height="653">

<h2 id="はじめに">はじめに</h2><p>はじめまして、フューチャーのインターン”Engineer Camp”に参加した平野と申します。<br>今回のインターンでは、Google Cloud Platform (GCP)のサービスとして提供されているDataflowについて調査し、その仕組みや使い方についてこの技術ブログにまとめることに取り組みました。</p>
<p>フューチャーのインターンについてはこちらをご覧ください！</p>
<p>今回の記事は前編・後編に分かれており</p>
<ul>
<li>前編:<ul>
<li>Dataflowの概要</li>
<li>Apache Beamの概要・内部的な仕組み</li>
<li>Apache Beamのコードの書き方</li>
</ul>
</li>
<li>後編：<ul>
<li>Dataflowを使う上での事前準備と基本的な使い方</li>
<li>GPUを使う上での事前準備と基本的な使い方</li>
<li>Pub&#x2F;Sub・BigQueryとの連携例</li>
</ul>
</li>
</ul>
<p>という構成になっています。前編はこちら。</p>
<h2 id="Datflowの事前準備と基本的な使い方">Datflowの事前準備と基本的な使い方</h2><p>Dataflowを使うための事前準備からパイプライン実行までの一連の流れについて説明します。以下の手順で進めます。</p>
<ol>
<li>APIの有効化</li>
<li>IAMの設定</li>
<li>Apache Beam SDKのインストール</li>
<li>Cloud Storageバケットの作成</li>
<li>Dataflow上でパイプラインを実行</li>
</ol>
<p>なお、以降の</p>
<ul>
<li>Dataflowの使用例（GPUなしver.）</li>
<li>DataflowでGPUを使う際の事前準備と基本的な使い方</li>
<li>Dataflowの使用例（GPUありver.）</li>
<li>他のGCPサービスとの連携とストリーミング処理</li>
</ul>
<p>では、ここで説明するAPIの有効化、IAMの設定、Cloud Storageバケットの作成ができている前提で話を進めています。</p>
<h3 id="APIの有効化">APIの有効化</h3><p>Compute Engine API, Dataflow API, Cloud Storage APIとその他必要な（連携させたい）APIを有効化します。APIの有効化はコンソール画面上部にある検索窓から有効化したいAPIを検索すれば簡単に有効化できます。</p>
<h3 id="IAMの設定">IAMの設定</h3><p>APIを有効化するとIAMに<strong>Compute Engine default service account</strong>という名前のアカウントが追加されているはずです。<br>Dataflowを利用するにはそのサービスアカウントに<strong>Dataflowワーカー</strong>、<strong>Dataflow管理者</strong>、<strong>Storageオブジェクト管理者</strong>のロールを追加して保存します。以下の画像のようになっていればOKです。<br><img src="/images/2022/20220920b/IAM_setting.png" alt="IAM_setting.png" width="1200" height="164" loading="lazy"><br>なお、ロールを付与するには、<strong>resourcemanager.projects.setIamPolicy</strong>の権限を持っている必要があります。持っていない場合はプロジェクトの管理者に権限を付与してもらうか、サービスアカウントへのロールの付与を代わりにやってもらってください。</p>
<h3 id="Apache-Beam-SDKのインストール">Apache Beam SDKのインストール</h3><p>続いて、ローカル環境（今回はCloud Shell）にApache Beam SDKをインストールします。2022&#x2F;08&#x2F;30現在、Apache Beam SDKでサポートされているPythonのバージョンは3.8までです。一方、Cloud ShellにデフォルトでインストールされているPythonのバージョンは3.9ですので、pyenv等を用いてPython3.8を実行する仮想環境を作成してください。その後、作成した仮想環境にApache Beamをインストールします。Dataflow(GCP)上で実行するには追加パッケージをインストールする必要があるので、以下のコマンドでインストールしてください。</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">pip3 install apache-beam[gcp]</span><br></pre></td></tr></table></figure>

<h3 id="Cloud-Storageバケットの作成">Cloud Storageバケットの作成</h3><p>Dataflowでパイプライン処理を行う場合、一時ファイルや出力ファイルを保存するためにCloud Storageのバケットを作成する必要があります。<br>バケットの作成はコンソール画面から作成する方法とPythonから作成する方法があります。<br>コンソール画面からは以下のように作成できます。<br><img src="/images/2022/20220920b/make_bucket_new.gif" alt="make_bucket_new.gif" width="1200" height="665" loading="lazy"></p>
<p>Pythonからバケットを作成する際は以下のコードを参考にしてください（<code>pip3 install google-cloud-storage</code>が必要です）。</p>
<figure class="highlight python"><table><tr><td class="code"><pre><span class="line"><span class="keyword">from</span> google.cloud <span class="keyword">import</span> storage</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">make_bucket</span>(<span class="params">project_name, bucket_name, region</span>):</span><br><span class="line">    client = storage.Client(project_name)</span><br><span class="line">    bucket = storage.Bucket(client)</span><br><span class="line">    bucket.name = bucket_name</span><br><span class="line">    <span class="keyword">if</span> <span class="keyword">not</span> bucket.exists():</span><br><span class="line">        client.create_bucket(bucket, location=region)</span><br></pre></td></tr></table></figure>

<h3 id="Dataflow上でパイプラインを実行">Dataflow上でパイプラインを実行</h3><p>続いて、Dataflow上でパイプラインを実行していきます。Dataflow上でパイプラインを実行するにはいくつかのオプションを指定する必要があります（主にGCP関連）。ここでは、それらのオプションの説明とオプションの渡し方について説明します。<br>Dataflowでパイプラインを実行するためには以下のようなオプションを指定する必要があります。</p>
<div class="scroll"><table>
<thead>
<tr>
<th align="center">オプション名</th>
<th align="left">説明</th>
</tr>
</thead>
<tbody><tr>
<td align="center">runner</td>
<td align="left">Dataflowで動かす場合には<code>DataflowRunner</code>を指定。ローカルで動かす場合には<code>DirectRunner</code>。</td>
</tr>
<tr>
<td align="center">project</td>
<td align="left">プロジェクトID。指定しないとエラーが返ってくる。</td>
</tr>
<tr>
<td align="center">job_name</td>
<td align="left">実行するジョブの名前。Dataflowのジョブのところにジョブの一覧が表示されるが、その際にどのジョブかを見分ける際に使える。指定しなければ勝手に名前をつけてくれるが、パッと見で判断しづらい。</td>
</tr>
<tr>
<td align="center">temp_location</td>
<td align="left">一時ファイルを保存するためのGCSのパス（<code>gs://</code>からスタートするパス）。指定しなければstaging_locationのパスが使用される。</td>
</tr>
<tr>
<td align="center">staging_location</td>
<td align="left">ローカルファイルをステージングするためのGCSのパス。指定しなければtemp_locationのパスが使用される。temp_locationかstaging_locationのどちらかは指定しなければならない。</td>
</tr>
<tr>
<td align="center">region</td>
<td align="left">Dataflowジョブをデプロイするリージョンエンドポイント。デフォルトでは<code>us-central1</code>。</td>
</tr>
</tbody></table></div>
<p>ここでは動かすのに必要な（とりあえずこのへんを渡しておけば動く）オプションを紹介していますので、その他のオプションについては公式ドキュメントを参照してください。</p>
<p>実行する際には以下のように<code>--&lt;オプション名&gt; 値</code>の形式で指定することでオプションを渡すことができます。</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">python &#123;ソースコードまでのpath&#125; \</span><br><span class="line">--runner <span class="string">&quot;DataflowRunner&quot;</span> \</span><br><span class="line">--project <span class="string">&quot;&#123;プロジェクトID&#125;&quot;</span> \</span><br><span class="line">--job_name <span class="string">&quot;&#123;ジョブの名前&#125;&quot;</span> \</span><br><span class="line">--temp_location <span class="string">&quot;gs://&#123;バケットの名前&#125;/temp&quot;</span> \</span><br><span class="line">--region <span class="string">&quot;asia-northeast1&quot;</span></span><br></pre></td></tr></table></figure>

<h2 id="Dataflowの使用例（GPUなしver-）">Dataflowの使用例（GPUなしver.）</h2><p>ここでは、scikit-learnのモデルの推論をDataflow上で行う例を扱っていきます。今回はIrisデータセットで学習したモデルの重みパラメータ(<code>SVC_iris.pkl2</code>)が既に手元にあるという想定で、そのモデルの推論（学習時と同じIrisデータセットを使用）をDataflow上で行っていきます。以下のような手順で進めていきます。</p>
<ol>
<li>ソースコードの準備</li>
<li>Cloud ShellでPythonの環境構築</li>
<li>パイプラインの実行</li>
</ol>
<p>なお、APIの有効化、IAMの設定、Cloud Storageバケットの作成がお済みでない方はまずそちらから始めてください。</p>
<h3 id="ソースコードの準備">ソースコードの準備</h3><p>今回実行したいソースコード(ファイル名:<code>runinference_sklearn.py</code>)です。モデルの重みパラメータまでのパスは<code>&#123;ソースコードがあるディレクトリ&#125;/models/sklearn_models/SVC_iris.pkl2</code>です。</p>
<figure class="highlight python"><figcaption><span>runinference_sklearn.py</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> logging</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> apache_beam <span class="keyword">as</span> beam</span><br><span class="line"><span class="keyword">from</span> apache_beam.ml.inference <span class="keyword">import</span> RunInference</span><br><span class="line"><span class="keyword">from</span> apache_beam.ml.inference.sklearn_inference <span class="keyword">import</span> ModelFileType, SklearnModelHandlerNumpy</span><br><span class="line"><span class="keyword">from</span> apache_beam.options.pipeline_options <span class="keyword">import</span> PipelineOptions</span><br><span class="line"><span class="keyword">from</span> google.cloud <span class="keyword">import</span> storage</span><br><span class="line"><span class="keyword">from</span> sklearn.datasets <span class="keyword">import</span> load_iris</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">upload_model_to_gcs</span>(<span class="params">local_model_path, gcs_model_path, project_name, bucket_name</span>):</span><br><span class="line">    client = storage.Client(project_name)</span><br><span class="line">    bucket = storage.Bucket(client)</span><br><span class="line">    bucket.name = bucket_name</span><br><span class="line">    blob = bucket.blob(gcs_model_path)</span><br><span class="line">    blob.upload_from_filename(local_model_path)</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> __name__ == <span class="string">&quot;__main__&quot;</span>:</span><br><span class="line">    <span class="comment"># パイプラインオプションの設定</span></span><br><span class="line">    pipeline_options = PipelineOptions()</span><br><span class="line">    options_dict = pipeline_options.display_data()</span><br><span class="line"></span><br><span class="line">    <span class="comment"># Irisデータの準備</span></span><br><span class="line">    data = load_iris()</span><br><span class="line">    numpy_data = data.data</span><br><span class="line"></span><br><span class="line">    <span class="comment"># モデルのアップロード</span></span><br><span class="line">    upload_model_to_gcs(</span><br><span class="line">        local_model_path=<span class="string">&quot;./models/sklearn_models/SVC_iris.pkl2&quot;</span>,</span><br><span class="line">        gcs_model_path=<span class="string">&quot;models/sklearn_models/SVC_iris.pkl2&quot;</span>,</span><br><span class="line">        project_name=options_dict[<span class="string">&quot;project&quot;</span>],</span><br><span class="line">        bucket_name=options_dict[<span class="string">&quot;bucket_name&quot;</span>]</span><br><span class="line">    )</span><br><span class="line"></span><br><span class="line">    <span class="comment"># ハンドラーの設定</span></span><br><span class="line">    model_uri = <span class="string">&quot;gs://&#123;&#125;/models/sklearn_models/SVC_iris.pkl2&quot;</span>.<span class="built_in">format</span>(options_dict[<span class="string">&quot;bucket_name&quot;</span>])</span><br><span class="line">    model_file_type = ModelFileType.JOBLIB</span><br><span class="line">    model_handler = SklearnModelHandlerNumpy(model_uri=model_uri, model_file_type=model_file_type)</span><br><span class="line"></span><br><span class="line">    <span class="comment"># パイプライン実行</span></span><br><span class="line">    logging.getLogger().setLevel(logging.INFO)</span><br><span class="line">    <span class="keyword">with</span> beam.Pipeline(options=pipeline_options) <span class="keyword">as</span> p:</span><br><span class="line">        <span class="built_in">input</span> = p | <span class="string">&quot;read&quot;</span> &gt;&gt; beam.Create(numpy_data)</span><br><span class="line"></span><br><span class="line">        prediction = (</span><br><span class="line">            <span class="built_in">input</span></span><br><span class="line">            | RunInference(model_handler)</span><br><span class="line">            | beam.io.WriteToText(options_dict[<span class="string">&quot;output_executable_path&quot;</span>], shard_name_template=<span class="string">&quot;&quot;</span>)</span><br><span class="line">        )</span><br></pre></td></tr></table></figure>

<h3 id="Cloud-ShellでPythonの環境構築">Cloud ShellでPythonの環境構築</h3><p>次にCloud ShellのPython環境を構築していきます。<br>まず、Python 3.8の環境を準備します。ターミナル上で…</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">pyenv install 3.8.13</span><br></pre></td></tr></table></figure>

<p>を実行し、Python 3.8をインストールします。その後、</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">pyenv virtualenv 3.8.13 dataflow</span><br><span class="line">pyenv activate dataflow</span><br></pre></td></tr></table></figure>

<p>を実行してPython 3.8.13がインストールされた仮想環境（ここでは<code>dataflow</code>）をアクティベートします。<br>続いて、必要なパッケージをインストールしていきます。</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">pip3 install apache-beam[gcp] google-gcloud-storage</span><br><span class="line">pip3 install scikit-learn</span><br></pre></td></tr></table></figure>

<h3 id="パイプラインの実行">パイプラインの実行</h3><p>必要なパッケージのインストールが終わったら、最後にパイプラインを実行していきます。以下のコマンドを実行するとDataflow上でパイプライン処理が動き始めます。<code>&#123;プロジェクトID&#125;</code>、<code>&#123;ジョブの名前&#125;</code>、<code>&#123;バケットの名前&#125;</code>は適宜変更してください。</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">python runinference_sklearn.py \</span><br><span class="line">--runner <span class="string">&quot;DataflowRunner&quot;</span> \</span><br><span class="line">--project <span class="string">&quot;&#123;プロジェクトID&#125;&quot;</span> \</span><br><span class="line">--job_name <span class="string">&quot;&#123;ジョブの名前&#125;&quot;</span> \</span><br><span class="line">--temp_location <span class="string">&quot;gs://&#123;バケットの名前&#125;/temp/&quot;</span> \</span><br><span class="line">--staging_location <span class="string">&quot;gs://&#123;バケットの名前&#125;/stage/&quot;</span> \</span><br><span class="line">--region <span class="string">&quot;asia-northeast1&quot;</span> \</span><br><span class="line">--bucket_name <span class="string">&quot;&#123;バケットの名前&#125;&quot;</span> \</span><br><span class="line">--output <span class="string">&quot;gs://&#123;バケットの名前&#125;/output.txt&quot;</span></span><br></pre></td></tr></table></figure>

<h3 id="結果">結果</h3><p>推論結果はCloud Storageのバケットの<code>output.txt</code>に出力されます。今回の例では以下のような結果が得られました。</p>
<figure class="highlight text"><figcaption><span>output.txt</span></figcaption><table><tr><td class="code"><pre><span class="line">PredictionResult(example=array([5.1, 3.4, 1.5, 0.2]), inference=0)</span><br><span class="line">PredictionResult(example=array([5. , 3.4, 1.6, 0.4]), inference=0)</span><br><span class="line">PredictionResult(example=array([7.6, 3. , 6.6, 2.1]), inference=2)</span><br><span class="line">PredictionResult(example=array([5.9, 3. , 4.2, 1.5]), inference=1)</span><br><span class="line">PredictionResult(example=array([5.7, 3.8, 1.7, 0.3]), inference=0)</span><br><span class="line">PredictionResult(example=array([5.7, 4.4, 1.5, 0.4]), inference=0)</span><br><span class="line">PredictionResult(example=array([6.9, 3.1, 5.4, 2.1]), inference=2)</span><br><span class="line">PredictionResult(example=array([6.2, 2.2, 4.5, 1.5]), inference=1)</span><br><span class="line">PredictionResult(example=array([5.2, 4.1, 1.5, 0.1]), inference=0)</span><br><span class="line">...</span><br></pre></td></tr></table></figure>

<h2 id="DataflowでGPUを使う際の事前準備と基本的な使い方">DataflowでGPUを使う際の事前準備と基本的な使い方</h2><p>DataflowでGPUを使用したい場合（例えば機械学習モデルの推論など）には、Dockerと組み合わせることでGPUを使用できます。<br>基本的な流れはDatflowの事前準備と基本的な使い方と同じです。違いはDockerイメージの準備とパイプラインに追加で渡すオプションが増えることくらいです。ここでは…</p>
<ol>
<li>Dockerイメージの準備</li>
<li>GPU使用時のオプション</li>
</ol>
<p>について説明します。なお、APIの有効化、IAMの設定、Cloud Storageバケットの作成がお済みでない方はまずそちらから始めてください。</p>
<h3 id="Dockerイメージの準備">Dockerイメージの準備</h3><p>DataflowでGPUを使用するには、Apache Beamが扱える、かつ、必要なGPUライブラリが入ったDockerイメージを用意する必要があります。ありがたいことにPyTorch用の最小イメージやTensorFlow用の最小イメージのためのsampleが既に用意されているので、特に理由がなければこちらを利用するのが楽かと思います。</p>
<p>PyTorchを使用する場合にはPyTorch用の最小イメージからファイルをダウンロード後、</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">gcloud builds submit --config build.yaml</span><br></pre></td></tr></table></figure>

<p>で、DockerイメージをContainer Registryに保存します（デフォルトでのイメージ名は<code>samples/dataflow/pytorch-gpu:latest</code>）。</p>
<p>なお、私の環境では、Pythonのバージョンが3.8ではパイプライン実行の際にエラー（<code>TypeError: code() takes at most 15 arguments (16 given)</code>）が発生してしまっていたため、Pythonのバージョンを3.7に落としました。具体的には以下のように変更することでエラーは発生しなくなりました。</p>
<ul>
<li><p>pyenvでPython 3.7の環境を用意<br>  ターミナル上で</p>
  <figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">pyenv install 3.7.13</span><br></pre></td></tr></table></figure>

<p>  を実行し、Python 3.7をインストールします。その後、</p>
  <figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">pyenv virtualenv 3.7.13 dataflow_gpu</span><br><span class="line">pyenv activate dataflow_gpu</span><br></pre></td></tr></table></figure>

<p>  を実行してPython 3.7.13がインストールされた仮想環境（ここでは<code>dataflow_gpu</code>）をアクティベートします。<br>  続いて、Apache Beamをインストールしていきます。</p>
  <figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">pip3 install apache-beam[gcp]</span><br></pre></td></tr></table></figure>
</li>
<li><p>Dockerfileを以下のように変更</p>
  <figure class="highlight plaintext"><table><tr><td class="code"><pre><span class="line">FROM pytorch/pytorch:1.9.1-cuda11.1-cudnn8-runtime</span><br><span class="line"></span><br><span class="line">WORKDIR /pipeline</span><br><span class="line"></span><br><span class="line">COPY requirements.txt .</span><br><span class="line">COPY *.py ./</span><br><span class="line"></span><br><span class="line">RUN apt-get update \</span><br><span class="line">    &amp;&amp; apt-get install -y --no-install-recommends g++ \</span><br><span class="line">    &amp;&amp; apt-get install -y curl \  # この行を追加</span><br><span class="line">        python3.7 \  # この行を追加</span><br><span class="line">        python3-distutils \  # この行を追加</span><br><span class="line">    &amp;&amp; rm -rf /var/lib/apt/lists/* \</span><br><span class="line">    # Install the pipeline requirements and check that there are no conflicts.</span><br><span class="line">    # Since the image already has all the dependencies installed,</span><br><span class="line">    # there&#x27;s no need to run with the --requirements_file option.</span><br><span class="line">    &amp;&amp; pip install --no-cache-dir --upgrade pip \</span><br><span class="line">    &amp;&amp; pip install --no-cache-dir -r requirements.txt \</span><br><span class="line">    &amp;&amp; pip check</span><br><span class="line"></span><br><span class="line"># Set the entrypoint to Apache Beam SDK worker launcher.</span><br><span class="line">COPY --from=apache/beam_python3.8_sdk:2.38.0 /opt/apache/beam /opt/apache/beam</span><br><span class="line">ENTRYPOINT [ &quot;/opt/apache/beam/boot&quot; ]</span><br></pre></td></tr></table></figure></li>
</ul>
<h3 id="GPU使用時のオプション">GPU使用時のオプション</h3><p>DataflowでGPUを使用する際には、実行時に以下のようなオプションを追加で指定する必要があります。</p>
<div class="scroll"><table>
<thead>
<tr>
<th align="left">オプション名</th>
<th align="left">説明</th>
</tr>
</thead>
<tbody><tr>
<td align="left">sdk_container_image</td>
<td align="left">使用するコンテナイメージの名前。</td>
</tr>
<tr>
<td align="left">disk_size_gb</td>
<td align="left">各ワーカー VM のブートディスクのサイズ</td>
</tr>
<tr>
<td align="left">experiments</td>
<td align="left">Dataflow Runner v2を使用するかやGPUのタイプ・個数、Nvidiaドライバをインストールするかを指定する際に使用。具体的な使い方は下の例を参照。</td>
</tr>
</tbody></table></div>
<p><code>experiments</code>オプションに関しては次のように指定します。下の例のように複数個に分けて指定してもOKです。</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">--experiments <span class="string">&quot;worker_accelerator=type:nvidia-tesla-t4;count:1;install-nvidia-driver&quot;</span> \</span><br><span class="line">--experiments <span class="string">&quot;use_runner_v2&quot;</span></span><br></pre></td></tr></table></figure>

<h2 id="Dataflowの使用例（GPUありver-）">Dataflowの使用例（GPUありver.）</h2><p>ここでは、PyTorchのモデルの推論をDataflow上で行う例を扱っていきます。今回はMNISTデータセットで学習したモデルの重みパラメータ(<code>mnist_epoch_10.pth</code>)が既に手元にあるという想定で、そのモデルの推論（MNISTのテスト用データセットを使用）をDataflow上で行っていきます。以下のような手順で進めていきます。</p>
<ol>
<li>ソースコードの準備</li>
<li>Dockerコンテナイメージの作成</li>
<li>Cloud ShellでPythonの環境構築</li>
<li>パイプラインの実行</li>
</ol>
<p>なお、APIの有効化、IAMの設定、Cloud Storageバケットの作成がお済みでない方はまずそちらから始めてください。</p>
<h3 id="ソースコードの準備-1">ソースコードの準備</h3><p>今回実行したいソースコード(ファイル名:<code>runinference_pytorch.py</code>)です。</p>
<figure class="highlight python"><figcaption><span>runinference_pytorch.py</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> logging</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> apache_beam <span class="keyword">as</span> beam</span><br><span class="line"><span class="keyword">from</span> apache_beam.ml.inference.base <span class="keyword">import</span> RunInference</span><br><span class="line"><span class="keyword">from</span> apache_beam.ml.inference.pytorch_inference <span class="keyword">import</span> PytorchModelHandlerTensor</span><br><span class="line"><span class="keyword">from</span> apache_beam.options.pipeline_options <span class="keyword">import</span> PipelineOptions</span><br><span class="line"><span class="keyword">from</span> google.cloud <span class="keyword">import</span> storage</span><br><span class="line"><span class="keyword">from</span> torchvision <span class="keyword">import</span> datasets, transforms</span><br><span class="line"></span><br><span class="line"><span class="keyword">from</span> pytorch_MNIST <span class="keyword">import</span> MNIST_Model</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">upload_model_to_gcs</span>(<span class="params">local_model_path, gcs_model_path, project_name, bucket_name</span>):</span><br><span class="line">    client = storage.Client(project_name)</span><br><span class="line">    bucket = storage.Bucket(client)</span><br><span class="line">    bucket.name = bucket_name</span><br><span class="line">    blob = bucket.blob(gcs_model_path)</span><br><span class="line">    blob.upload_from_filename(local_model_path)</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> __name__ == <span class="string">&quot;__main__&quot;</span>:</span><br><span class="line">    <span class="comment"># パイプラインオプションの設定</span></span><br><span class="line">    pipeline_options = PipelineOptions()</span><br><span class="line">    options_dict = pipeline_options.display_data()</span><br><span class="line"></span><br><span class="line">    <span class="comment"># データセットの準備</span></span><br><span class="line">    transform = transforms.Compose([</span><br><span class="line">        transforms.ToTensor(),</span><br><span class="line">        transforms.Normalize((<span class="number">0.1307</span>), (<span class="number">0.3081</span>), inplace=<span class="literal">True</span>)</span><br><span class="line">    ])</span><br><span class="line">    test_dataset = datasets.MNIST(</span><br><span class="line">        root=<span class="string">&quot;./data/&quot;</span>,</span><br><span class="line">        train=<span class="literal">False</span>,</span><br><span class="line">        transform=transform,</span><br><span class="line">        download=<span class="literal">True</span></span><br><span class="line">    )</span><br><span class="line"></span><br><span class="line">    <span class="comment"># モデルのアップロード</span></span><br><span class="line">    upload_model_to_gcs(</span><br><span class="line">        local_model_path=<span class="string">&quot;./models/pytorch_models/mnist_epoch_10.pth&quot;</span>,</span><br><span class="line">        gcs_model_path=<span class="string">&quot;models/pytorch_models/mnist_epoch_10.pth&quot;</span>,</span><br><span class="line">        project_name=options_dict[<span class="string">&quot;project&quot;</span>],</span><br><span class="line">        bucket_name=options_dict[<span class="string">&quot;bucket_name&quot;</span>]</span><br><span class="line">    )</span><br><span class="line"></span><br><span class="line">    <span class="comment"># ハンドラーの設定</span></span><br><span class="line">    model_handler = PytorchModelHandlerTensor(</span><br><span class="line">        state_dict_path=<span class="string">&quot;gs://&#123;&#125;/models/pytorch_models/mnist_epoch_10.pth&quot;</span>.<span class="built_in">format</span>(options_dict[<span class="string">&quot;bucket_name&quot;</span>]),</span><br><span class="line">        model_class=MNIST_Model,</span><br><span class="line">        model_params=&#123;&#125;,</span><br><span class="line">        device=<span class="string">&quot;GPU&quot;</span></span><br><span class="line">    )</span><br><span class="line"></span><br><span class="line">    <span class="comment"># パイプライン実行</span></span><br><span class="line">    logging.getLogger().setLevel(logging.INFO)</span><br><span class="line">    <span class="keyword">with</span> beam.Pipeline(options=pipeline_options) <span class="keyword">as</span> p:</span><br><span class="line">        data = p | <span class="string">&quot;read&quot;</span> &gt;&gt; beam.Create(test_dataset)</span><br><span class="line">        test = (</span><br><span class="line">            data</span><br><span class="line">            | <span class="string">&quot;extract image&quot;</span> &gt;&gt; beam.Map(<span class="keyword">lambda</span> x: x[<span class="number">0</span>])</span><br><span class="line">            | <span class="string">&quot;inference&quot;</span> &gt;&gt; RunInference(model_handler)</span><br><span class="line">            | beam.io.WriteToText(options_dict[<span class="string">&quot;output_executable_path&quot;</span>], shard_name_template=<span class="string">&quot;&quot;</span>)</span><br><span class="line">        )</span><br><span class="line"></span><br></pre></td></tr></table></figure>

<p>モデルの構造を定義したコード(ファイル名:<code>pytorch_MNIST.py</code>)です。</p>
<figure class="highlight python"><figcaption><span>pytorch_MNIST.py</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="keyword">from</span> torch <span class="keyword">import</span> nn</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">MNIST_Model</span>(nn.Module):</span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">__init__</span>(<span class="params">self</span>):</span><br><span class="line">        <span class="built_in">super</span>().__init__()</span><br><span class="line">        <span class="variable language_">self</span>.feature = nn.Sequential(</span><br><span class="line">            nn.Conv2d(<span class="number">1</span>, <span class="number">3</span>, <span class="number">3</span>, padding=<span class="number">1</span>),</span><br><span class="line">            nn.MaxPool2d(<span class="number">2</span>, <span class="number">2</span>),</span><br><span class="line">            nn.ReLU(<span class="literal">True</span>),</span><br><span class="line">            nn.Conv2d(<span class="number">3</span>, <span class="number">3</span>, <span class="number">3</span>, padding=<span class="number">1</span>),</span><br><span class="line">            nn.MaxPool2d(<span class="number">2</span>, <span class="number">2</span>),</span><br><span class="line">            nn.ReLU(<span class="literal">True</span>)</span><br><span class="line">        )</span><br><span class="line">        <span class="variable language_">self</span>.classifier = nn.Sequential(</span><br><span class="line">            nn.Linear(<span class="number">147</span>, <span class="number">128</span>),</span><br><span class="line">            nn.ReLU(<span class="literal">True</span>),</span><br><span class="line">            nn.Linear(<span class="number">128</span>, <span class="number">10</span>)</span><br><span class="line">        )</span><br><span class="line"></span><br><span class="line">    <span class="keyword">def</span> <span class="title function_">forward</span>(<span class="params">self, x</span>):</span><br><span class="line">        x = <span class="variable language_">self</span>.feature(x)</span><br><span class="line">        x = x.view(x.size(<span class="number">0</span>), -<span class="number">1</span>)</span><br><span class="line">        x = <span class="variable language_">self</span>.classifier(x)</span><br><span class="line">        <span class="keyword">return</span> x</span><br></pre></td></tr></table></figure>

<p>これらのソースコードはCloud Shellの同一のディレクトリに置いてください。また、モデルの重みパラメータまでのパスは<code>&#123;ソースコードがあるディレクトリ&#125;/models/pytorch_models/mnist_epoch_10.pth</code>です。</p>
<h3 id="Dockerコンテナイメージの作成">Dockerコンテナイメージの作成</h3><p>続いて、Dockerイメージを準備していきます。PyTorch用の最小イメージからファイルをダウンロード後、それらのファイルをソースコードと同一のディレクトリに置きます。続いてDockerfileを以下のように変更します。</p>
<figure class="highlight dockerfile"><figcaption><span>Dockerfile</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="keyword">FROM</span> pytorch/pytorch:<span class="number">1.9</span>.<span class="number">1</span>-cuda11.<span class="number">1</span>-cudnn8-runtime</span><br><span class="line"></span><br><span class="line"><span class="keyword">WORKDIR</span><span class="language-bash"> /pipeline</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">COPY</span><span class="language-bash"> requirements.txt .</span></span><br><span class="line"><span class="keyword">COPY</span><span class="language-bash"> *.py ./</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">RUN</span><span class="language-bash"> apt-get update \</span></span><br><span class="line"><span class="language-bash">    &amp;&amp; apt-get install -y --no-install-recommends g++ \</span></span><br><span class="line"><span class="language-bash">    &amp;&amp; apt-get install -y curl \  <span class="comment"># この行を追加</span></span></span><br><span class="line">        python3.<span class="number">7</span> \  <span class="comment"># この行を追加</span></span><br><span class="line">        python3-distutils \  <span class="comment"># この行を追加</span></span><br><span class="line">    &amp;&amp; rm -rf /var/lib/apt/lists/* \</span><br><span class="line">    <span class="comment"># Install the pipeline requirements and check that there are no conflicts.</span></span><br><span class="line">    <span class="comment"># Since the image already has all the dependencies installed,</span></span><br><span class="line">    <span class="comment"># there&#x27;s no need to run with the --requirements_file option.</span></span><br><span class="line">    &amp;&amp; pip install --no-cache-dir --upgrade pip \</span><br><span class="line">    &amp;&amp; pip install --no-cache-dir -r requirements.txt \</span><br><span class="line">    &amp;&amp; pip check</span><br><span class="line"></span><br><span class="line"><span class="comment"># Set the entrypoint to Apache Beam SDK worker launcher.</span></span><br><span class="line"><span class="keyword">COPY</span><span class="language-bash"> --from=apache/beam_python3.8_sdk:2.38.0 /opt/apache/beam /opt/apache/beam</span></span><br><span class="line"><span class="keyword">ENTRYPOINT</span><span class="language-bash"> [ <span class="string">&quot;/opt/apache/beam/boot&quot;</span> ]</span></span><br></pre></td></tr></table></figure>

<p>その後、コンテナイメージをContainer Registryに保存するために以下のコマンドを実行します。</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">gcloud builds submit --config build.yaml</span><br></pre></td></tr></table></figure>

<p>コンテナイメージ名は<code>samples/dataflow/pytorch-gpu:latest</code>で保存されます。</p>
<h3 id="Cloud-ShellでPythonの環境構築-1">Cloud ShellでPythonの環境構築</h3><p>次にCloud ShellのPython環境を構築していきます。<br>まず、Python 3.7の環境を準備します。ターミナル上で…</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">pyenv install 3.7.13</span><br></pre></td></tr></table></figure>

<p>を実行し、Python 3.7をインストールします。その後、</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">pyenv virtualenv 3.7.13 dataflow_gpu</span><br><span class="line">pyenv activate dataflow_gpu</span><br></pre></td></tr></table></figure>

<p>を実行してPython 3.7.13がインストールされた仮想環境（ここでは<code>dataflow_gpu</code>）をアクティベートします。<br>続いて、必要なパッケージをインストールしていきます。</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">pip3 install apache-beam[gcp] google-gcloud-storage</span><br><span class="line">pip3 install torch torchvision</span><br></pre></td></tr></table></figure>

<h3 id="パイプラインの実行-1">パイプラインの実行</h3><p>必要なパッケージのインストールが終わったら、最後にパイプラインを実行していきます。<br>以下のコマンドを実行するとDataflow上でパイプライン処理が動き始めます。<code>&#123;プロジェクトID&#125;</code>、<code>&#123;ジョブの名前&#125;</code>、<code>&#123;バケットの名前&#125;</code>は適宜変更してください。</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">python runinference_pytorch.py \</span><br><span class="line">--runner <span class="string">&quot;DataflowRunner&quot;</span> \</span><br><span class="line">--project <span class="string">&quot;&#123;プロジェクトID&#125;&quot;</span> \</span><br><span class="line">--job_name <span class="string">&quot;&#123;ジョブの名前&#125;&quot;</span> \</span><br><span class="line">--temp_location <span class="string">&quot;gs://&#123;バケットの名前&#125;/temp/&quot;</span> \</span><br><span class="line">--staging_location <span class="string">&quot;gs://&#123;バケットの名前&#125;/stage/&quot;</span> \</span><br><span class="line">--region <span class="string">&quot;asia-northeast1&quot;</span> \</span><br><span class="line">--bucket_name <span class="string">&quot;&#123;バケットの名前&#125;&quot;</span> \</span><br><span class="line">--output <span class="string">&quot;gs://&#123;バケットの名前&#125;/output.txt&quot;</span> \</span><br><span class="line">--sdk_container_image <span class="string">&quot;gcr.io/&#123;プロジェクトID&#125;/samples/dataflow/pytorch-gpu:latest&quot;</span> \</span><br><span class="line">--disk_size_gb 50 \</span><br><span class="line">--experiments <span class="string">&quot;worker_accelerator=type:nvidia-tesla-t4;count:1;install-nvidia-driver&quot;</span> \</span><br><span class="line">--experiments <span class="string">&quot;use_runner_v2&quot;</span></span><br></pre></td></tr></table></figure>

<h3 id="結果-1">結果</h3><p>推論結果はCloud Storageのバケットの<code>output.txt</code>に出力されます。今回の例では以下のような結果が得られました。</p>
<figure class="highlight text"><figcaption><span>output.txt</span></figcaption><table><tr><td class="code"><pre><span class="line">tensor([ -8.2468,  -2.1803,  -9.8459,   1.3747,   2.4845,  -5.6996, -18.9429,</span><br><span class="line">          3.0085,  -5.7692,  12.0357], requires_grad=True)</span><br><span class="line">tensor([ -5.9876, -14.5651,  -7.3873,   8.2820,  -6.1497,   1.6121, -18.5136,</span><br><span class="line">         -9.5785,   1.7698,  12.8093], requires_grad=True)</span><br><span class="line">tensor([  9.2505,  -1.7219,  -2.7147,  -3.9045, -10.8319,  -1.9610,   2.5355,</span><br><span class="line">         -8.6489,  -3.3169,  -6.9540], requires_grad=True)</span><br><span class="line">tensor([-8.1391, -0.9647, -6.3984,  2.4964, -0.9498,  1.4407, -8.2989, -3.1957,</span><br><span class="line">         2.5867,  2.6507], requires_grad=True)</span><br><span class="line">tensor([-7.6571, -2.4950, -5.2014, -1.6730, 10.1947, -7.5948, -9.2541,  0.5039,</span><br><span class="line">        -2.6531,  7.1487], requires_grad=True)</span><br><span class="line">tensor([ -5.8362,  12.8431,  -4.1835,  -8.8176,  -6.0804, -10.7981,  -6.2982,</span><br><span class="line">         -0.1830,  -1.4379,  -4.4298], requires_grad=True)</span><br><span class="line">tensor([-4.6527, -7.1966, -8.8277, -7.4921,  6.7380, -4.9899, -0.2908, -4.7030,</span><br><span class="line">         2.0198,  2.2414], requires_grad=True)</span><br><span class="line">tensor([-9.9818, -9.7239, -4.4335, -2.8926,  7.8835,  1.4599, -1.7376, -6.2337,</span><br><span class="line">        -0.9638, -0.7414], requires_grad=True)</span><br><span class="line">tensor([ -3.8291,  -2.5081,  16.6454,   6.6208,  -7.5311, -10.9999, -13.9144,</span><br><span class="line">         -5.1685,   2.5498,  -7.2168], requires_grad=True)</span><br><span class="line">...</span><br></pre></td></tr></table></figure>

<h2 id="他のGCPサービスとの連携とストリーミング処理">他のGCPサービスとの連携とストリーミング処理</h2><p>最後に、Pub&#x2F;Subからリアルタイムにデータを取得→Dataflowでデータ処理→結果をBigQueryに書き出す例を紹介します。<br>今回はIrisデータセットの各サンプルを10秒間隔でPub&#x2F;SubにPublishし、Dataflowの使用例（GPUなしver.）で行ったscikit-learnモデルを用いた推論をストリーミング処理でDataflow上で行い、その結果をBigQueryに書き出します。今回もIrisデータセットで学習したモデルの重みパラメータ(<code>SVC_iris.pkl2</code>)が既に手元にあるという想定で、以下のような手順で進めていきます。</p>
<ol>
<li>ソースコードの準備</li>
<li>Pub&#x2F;Sub・BigQueryの準備</li>
<li>パイプラインの実行</li>
</ol>
<p>なお、APIの有効化、IAMの設定、Cloud Storageバケットの作成がお済みでない方はまずそちらから始めてください。</p>
<h3 id="ソースコードの準備-2">ソースコードの準備</h3><p>今回実行したいソースコード(ファイル名:<code>predict_iris_dataflow_pubsub2bq.py</code>)です。<br>モデルの重みパラメータまでのパスは<code>&#123;ソースコードがあるディレクトリ&#125;/models/sklearn_models/SVC_iris.pkl2</code>です。</p>
<figure class="highlight python"><figcaption><span>predict_iris_dataflow_pubsub2bq.py</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> json</span><br><span class="line"><span class="keyword">import</span> logging</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> apache_beam <span class="keyword">as</span> beam</span><br><span class="line"><span class="keyword">from</span> apache_beam.ml.inference <span class="keyword">import</span> RunInference</span><br><span class="line"><span class="keyword">from</span> apache_beam.ml.inference.sklearn_inference <span class="keyword">import</span> ModelFileType, SklearnModelHandlerNumpy</span><br><span class="line"><span class="keyword">from</span> apache_beam.options.pipeline_options <span class="keyword">import</span> PipelineOptions, StandardOptions</span><br><span class="line"><span class="keyword">from</span> google.cloud <span class="keyword">import</span> storage</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">upload_model_to_gcs</span>(<span class="params">local_model_path, gcs_model_path, project_name, bucket_name</span>):</span><br><span class="line">    client = storage.Client(project_name)</span><br><span class="line">    bucket = storage.Bucket(client)</span><br><span class="line">    bucket.name = bucket_name</span><br><span class="line">    blob = bucket.blob(gcs_model_path)</span><br><span class="line">    blob.upload_from_filename(local_model_path)</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> __name__ == <span class="string">&quot;__main__&quot;</span>:</span><br><span class="line">    <span class="comment"># パイプラインオプションの設定</span></span><br><span class="line">    options = PipelineOptions()</span><br><span class="line">    options_dict = options.display_data()</span><br><span class="line">    options.view_as(StandardOptions).runner = <span class="string">&quot;DataflowRunner&quot;</span></span><br><span class="line">    options.view_as(StandardOptions).streaming = <span class="literal">True</span></span><br><span class="line"></span><br><span class="line">    <span class="comment"># モデルのアップロード</span></span><br><span class="line">    upload_model_to_gcs(</span><br><span class="line">        local_model_path=<span class="string">&quot;./models/sklearn_models/SVC_iris.pkl2&quot;</span>,</span><br><span class="line">        gcs_model_path=<span class="string">&quot;models/sklearn_models/SVC_iris.pkl2&quot;</span>,</span><br><span class="line">        project_name=options_dict[<span class="string">&quot;project&quot;</span>],</span><br><span class="line">        bucket_name=options_dict[<span class="string">&quot;bucket_name&quot;</span>]</span><br><span class="line">    )</span><br><span class="line"></span><br><span class="line">    <span class="comment"># ハンドラーの設定</span></span><br><span class="line">    model_uri = <span class="string">&quot;gs://&#123;&#125;/models/sklearn_models/SVC_iris.pkl2&quot;</span>.<span class="built_in">format</span>(options_dict[<span class="string">&quot;bucket_name&quot;</span>])</span><br><span class="line">    model_file_type = ModelFileType.JOBLIB</span><br><span class="line">    model_handler = SklearnModelHandlerNumpy(model_uri=model_uri, model_file_type=model_file_type)</span><br><span class="line"></span><br><span class="line">    topic = <span class="string">&quot;projects/&#123;&#125;/topics/&#123;&#125;&quot;</span>.<span class="built_in">format</span>(options_dict[<span class="string">&quot;project&quot;</span>], options_dict[<span class="string">&quot;topic_name&quot;</span>])</span><br><span class="line"></span><br><span class="line">    <span class="comment"># パイプライン実行</span></span><br><span class="line">    logging.getLogger().setLevel(logging.INFO)</span><br><span class="line">    <span class="keyword">with</span> beam.Pipeline(options=options) <span class="keyword">as</span> p:</span><br><span class="line">        raw_data = (</span><br><span class="line">            p</span><br><span class="line">            | <span class="string">&quot;ReadFromPub/Sub&quot;</span> &gt;&gt; beam.io.ReadFromPubSub(topic)</span><br><span class="line">            | <span class="string">&quot;Decode&quot;</span> &gt;&gt; beam.Map(<span class="keyword">lambda</span> x: x.decode())</span><br><span class="line">            | <span class="string">&quot;StrToDict&quot;</span> &gt;&gt; beam.Map(json.loads)</span><br><span class="line">        )</span><br><span class="line"></span><br><span class="line">        inference = (</span><br><span class="line">            raw_data</span><br><span class="line">            | <span class="string">&quot;ExtractFeature&quot;</span> &gt;&gt; beam.Map(<span class="keyword">lambda</span> x: x[<span class="string">&quot;feature&quot;</span>])</span><br><span class="line">            | <span class="string">&quot;RunInference&quot;</span> &gt;&gt; RunInference(model_handler)</span><br><span class="line">        )</span><br><span class="line"></span><br><span class="line">        write2bq = (</span><br><span class="line">            inference</span><br><span class="line">            | <span class="string">&quot;ConvertToBigQueryFormat&quot;</span> &gt;&gt; beam.Map(<span class="keyword">lambda</span> x: &#123;</span><br><span class="line">                <span class="string">&quot;input&quot;</span>: &#123;</span><br><span class="line">                    <span class="string">&quot;sepal_length&quot;</span>: x[<span class="number">0</span>][<span class="number">0</span>],</span><br><span class="line">                    <span class="string">&quot;sepal_width&quot;</span>: x[<span class="number">0</span>][<span class="number">1</span>],</span><br><span class="line">                    <span class="string">&quot;petal_length&quot;</span>: x[<span class="number">0</span>][<span class="number">2</span>],</span><br><span class="line">                    <span class="string">&quot;petal_width&quot;</span>: x[<span class="number">0</span>][<span class="number">3</span>]</span><br><span class="line">                &#125;,</span><br><span class="line">                <span class="string">&quot;predict&quot;</span>: x[<span class="number">1</span>].item()</span><br><span class="line">            &#125;)</span><br><span class="line">            | <span class="string">&quot;WriteToBigQuery&quot;</span> &gt;&gt; beam.io.WriteToBigQuery(table=options_dict[<span class="string">&quot;table_name&quot;</span>], dataset=options_dict[<span class="string">&quot;dataset_name&quot;</span>])</span><br><span class="line">        )</span><br></pre></td></tr></table></figure>

<p>また、Irisデータセットの各サンプルを10秒間隔でPub&#x2F;SubにPublishにするためのコード（ファイル名:<code>publish_iris_local2pubsub.py</code>）です。</p>
<figure class="highlight python"><figcaption><span>publish_iris_local2pubsub.py</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> argparse</span><br><span class="line"><span class="keyword">import</span> json</span><br><span class="line"><span class="keyword">import</span> time</span><br><span class="line"></span><br><span class="line"><span class="keyword">from</span> google.cloud <span class="keyword">import</span> pubsub</span><br><span class="line"><span class="keyword">from</span> sklearn.datasets <span class="keyword">import</span> load_iris</span><br><span class="line"></span><br><span class="line">parser = argparse.ArgumentParser()</span><br><span class="line">parser.add_argument(<span class="string">&quot;--project&quot;</span>, required=<span class="literal">True</span>)</span><br><span class="line">parser.add_argument(<span class="string">&quot;--topic_name&quot;</span>, required=<span class="literal">True</span>)</span><br><span class="line"></span><br><span class="line">args = parser.parse_args()</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> __name__ == <span class="string">&quot;__main__&quot;</span>:</span><br><span class="line">    data = load_iris()</span><br><span class="line">    feature = data.data</span><br><span class="line">    target = data.target</span><br><span class="line"></span><br><span class="line">    publisher = pubsub.PublisherClient()</span><br><span class="line">    topic_path = publisher.topic_path(args.project, args.topic_name)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> i, (f, t) <span class="keyword">in</span> <span class="built_in">enumerate</span>(<span class="built_in">zip</span>(feature, target)):</span><br><span class="line">        f_t_dict = &#123;<span class="string">&quot;id&quot;</span>: i, <span class="string">&quot;feature&quot;</span>: f.tolist(), <span class="string">&quot;target&quot;</span>: t.item()&#125;</span><br><span class="line">        message = json.dumps(f_t_dict)</span><br><span class="line">        <span class="built_in">print</span>(message)</span><br><span class="line">        b_message = message.encode()</span><br><span class="line">        publisher.publish(topic_path, b_message)</span><br><span class="line">        time.sleep(<span class="number">10</span>)</span><br></pre></td></tr></table></figure>

<h3 id="Pub-Sub・BigQueryの準備">Pub&#x2F;Sub・BigQueryの準備</h3><p>まず、Pub&#x2F;Subのトピック作成から始めていきます。Pub&#x2F;Subのページ上部にある「トピックを作成」から、トピックIDを設定してトピックを作成します。そのほかの設定に関しては今回はデフォルトのままで大丈夫です。<br><img src="/images/2022/20220920b/make_topic.png" alt="make_topic.png" width="1200" height="691" loading="lazy"></p>
<p>続いて、BigQueryのデータセット・テーブルの作成に入ります。BigQueryのデータセット・テーブルは以下のようにして作成できます。<br><img src="/images/2022/20220920b/make_dataset.gif" alt="make_dataset.gif" width="1200" height="675" loading="lazy"></p>
<p>なお、今回使用しているスキーマは以下の通りです。</p>
<figure class="highlight json"><table><tr><td class="code"><pre><span class="line"><span class="punctuation">[</span></span><br><span class="line">    <span class="punctuation">&#123;</span></span><br><span class="line">        <span class="attr">&quot;name&quot;</span><span class="punctuation">:</span> <span class="string">&quot;input&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;type&quot;</span><span class="punctuation">:</span> <span class="string">&quot;RECORD&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;mode&quot;</span><span class="punctuation">:</span> <span class="string">&quot;NULLABLE&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;fields&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">            <span class="punctuation">&#123;</span></span><br><span class="line">                <span class="attr">&quot;name&quot;</span><span class="punctuation">:</span> <span class="string">&quot;sepal_length&quot;</span><span class="punctuation">,</span></span><br><span class="line">                <span class="attr">&quot;type&quot;</span><span class="punctuation">:</span> <span class="string">&quot;FLOAT&quot;</span><span class="punctuation">,</span></span><br><span class="line">                <span class="attr">&quot;mode&quot;</span><span class="punctuation">:</span> <span class="string">&quot;NULLABLE&quot;</span></span><br><span class="line">            <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">            <span class="punctuation">&#123;</span></span><br><span class="line">                <span class="attr">&quot;name&quot;</span><span class="punctuation">:</span> <span class="string">&quot;sepal_width&quot;</span><span class="punctuation">,</span></span><br><span class="line">                <span class="attr">&quot;type&quot;</span><span class="punctuation">:</span> <span class="string">&quot;FLOAT&quot;</span><span class="punctuation">,</span></span><br><span class="line">                <span class="attr">&quot;mode&quot;</span><span class="punctuation">:</span> <span class="string">&quot;NULLABLE&quot;</span></span><br><span class="line">            <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">            <span class="punctuation">&#123;</span></span><br><span class="line">                <span class="attr">&quot;name&quot;</span><span class="punctuation">:</span> <span class="string">&quot;petal_length&quot;</span><span class="punctuation">,</span></span><br><span class="line">                <span class="attr">&quot;type&quot;</span><span class="punctuation">:</span> <span class="string">&quot;FLOAT&quot;</span><span class="punctuation">,</span></span><br><span class="line">                <span class="attr">&quot;mode&quot;</span><span class="punctuation">:</span> <span class="string">&quot;NULLABLE&quot;</span></span><br><span class="line">            <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">            <span class="punctuation">&#123;</span></span><br><span class="line">                <span class="attr">&quot;name&quot;</span><span class="punctuation">:</span> <span class="string">&quot;petal_width&quot;</span><span class="punctuation">,</span></span><br><span class="line">                <span class="attr">&quot;type&quot;</span><span class="punctuation">:</span> <span class="string">&quot;FLOAT&quot;</span><span class="punctuation">,</span></span><br><span class="line">                <span class="attr">&quot;mode&quot;</span><span class="punctuation">:</span> <span class="string">&quot;NULLABLE&quot;</span></span><br><span class="line">            <span class="punctuation">&#125;</span></span><br><span class="line">        <span class="punctuation">]</span></span><br><span class="line">    <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="punctuation">&#123;</span></span><br><span class="line">        <span class="attr">&quot;name&quot;</span><span class="punctuation">:</span> <span class="string">&quot;predict&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;type&quot;</span><span class="punctuation">:</span> <span class="string">&quot;INTEGER&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;mode&quot;</span><span class="punctuation">:</span> <span class="string">&quot;NULLABLE&quot;</span></span><br><span class="line">    <span class="punctuation">&#125;</span></span><br><span class="line"><span class="punctuation">]</span></span><br></pre></td></tr></table></figure>

<h3 id="パイプラインの実行-2">パイプラインの実行</h3><p>続いて、パイプラインの実行に移ります。以下のコマンドを実行するとパイプラインが動き始めます。<code>&#123;プロジェクトID&#125;</code>、<code>&#123;ジョブの名前&#125;</code>、<code>&#123;バケットの名前&#125;</code>、<code>&#123;テーブルの名前&#125;</code>、<code>&#123;データセットの名前&#125;</code>、<code>&#123;トピックの名前&#125;</code>は適宜変更してください。今回はRunnerおよびストリーミング処理のオプションはコード内で記述しているためコマンドライン引数から渡す必要はありません。ストリーミング処理をコマンドラインから有効化したい場合は、<code>--streaming</code>を加えるとできます。</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">python predict_iris_dataflow_pubsub2bq.py \</span><br><span class="line">--project <span class="string">&quot;&#123;プロジェクトID&#125;&quot;</span> \</span><br><span class="line">--job_name <span class="string">&quot;&#123;ジョブの名前&#125;&quot;</span> \</span><br><span class="line">--temp_location <span class="string">&quot;gs://&#123;バケットの名前&#125;/temp/&quot;</span> \</span><br><span class="line">--staging_location <span class="string">&quot;gs://&#123;バケットの名前&#125;/stage/&quot;</span> \</span><br><span class="line">--region <span class="string">&quot;asia-northeast1&quot;</span> \</span><br><span class="line">--bucket_name <span class="string">&quot;&#123;バケットの名前&#125;&quot;</span> \</span><br><span class="line">--table_name <span class="string">&quot;&#123;テーブルの名前&#125;&quot;</span> \</span><br><span class="line">--dataset_name <span class="string">&quot;&#123;データセットの名前&#125;&quot;</span> \</span><br><span class="line">--topic_name <span class="string">&quot;&#123;トピックの名前&#125;&quot;</span></span><br></pre></td></tr></table></figure>

<p>これでパイプラインが実行されます。</p>
<p>パイプラインのジョブが動き始めたら、以下のコマンドで、Irisデータセットの各サンプルをPublishしていきます。なお、PythonファイルからPub&#x2F;SubにPublishする際にはサービスアカウントキー作成する必要があります。<code>IAMと管理→サービスアカウント</code>からサービスアカウントキーを含むjsonファイルを作成し…</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line"><span class="built_in">export</span> GOOGLE_APPLICATION_CREDENTIALS=<span class="string">&quot;&#123;jsonファイルまでのpath&#125;&quot;</span></span><br></pre></td></tr></table></figure>

<p>で、PythonファイルからPub&#x2F;SubにPublishできるようになります。それが終わったら…</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">python publish_iris_local2pubsub.py \</span><br><span class="line">--project <span class="string">&quot;&#123;プロジェクトID&#125;&quot;</span> \</span><br><span class="line">--topic_name <span class="string">&quot;&#123;トピックの名前&#125;&quot;</span></span><br></pre></td></tr></table></figure>

<p>を実行して、Pub&#x2F;Subに10秒間隔でデータを送ります。</p>
<h3 id="結果-2">結果</h3><p>BigQueryの画面からクエリを実行して結果を確認します。クエリは下図の赤枠の部分を順にクリックして<br><img src="/images/2022/20220920b/make_query.png" alt="make_query.png" width="702" height="486" loading="lazy"></p>
<p>開いたエディタに…</p>
<figure class="highlight sql"><table><tr><td class="code"><pre><span class="line"><span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> `&#123;プロジェクトID&#125;.&#123;データセットの名前&#125;.&#123;テーブルの名前&#125;` LIMIT <span class="number">1000</span></span><br></pre></td></tr></table></figure>

<p>を入力して実行します。</p>
<p>今回の例では以下のような結果が得られました。<br><img src="/images/2022/20220920b/pubsub2bq_result.png" alt="pubsub2bq_result" width="1164" height="822" loading="lazy"></p>
<h2 id="さいごに">さいごに</h2><p>今回のインターンで扱わせていただいたDataflowは、なかなか個人で扱う機会がない一方で、ビジネスの場面ではとても需要のあるサービスです。そのようなものを扱う機会を頂けたことは今回のインターンに参加してよかったと思えることの１つです。また、私は今まで技術ブログを書いた経験がなかったため、今回のインターンで、学んだことを言語化しまとめることの難しさを知ることができました。</p>
<p>そのほかにも、インターンではSAIG（フューチャーのAIチーム）の進捗報告会に参加させていただき、さまざまなプロジェクトの存在、各プロジェクトの進め方、各プロジェクトの難しさなど実際の仕事の現場を体験できました。また、インターンのイベントの一環である社員の方にインタビューをさせていただき、そこでは専門分野の勉強の進め方、AIのトレンドのキャッチアップのやり方を教えていただきました。</p>
<p>今回のインターンでは本当に多くのことを学ばせていただきました。受け入れ先プロジェクトの方々やフューチャーHRの皆さん、本当にありがとうございました！</p>
<h2 id="参考">参考</h2><ul>
<li>Apache Beam (Dataflow) 実践入門【Python】</li>
<li>How Beam executes a pipeline (公式ドキュメント)</li>
<li>Python を使用して Dataflow パイプラインを作成する</li>
<li>GPUの使用</li>
</ul>
<p>アイキャッチはPaul BrennanによるPixabayからの画像です。</p>
]]></content>
    <summary type="html">Dataflowを使うための事前準備からパイプライン実行までの一連の流れについて説明します。次の手順で進めていきます。APIの有効化、IAMの設定、Apache Beam SDKのインストール...</summary>
    <category term="DataEngineering" scheme="https://future-architect.github.io/categories/DataEngineering/"/>
    <category term="BigQuery" scheme="https://future-architect.github.io/tags/BigQuery/"/>
    <category term="Dataflow" scheme="https://future-architect.github.io/tags/Dataflow/"/>
    <category term="GoogleCloud" scheme="https://future-architect.github.io/tags/GoogleCloud/"/>
    <category term="インターン" scheme="https://future-architect.github.io/tags/%E3%82%A4%E3%83%B3%E3%82%BF%E3%83%BC%E3%83%B3/"/>
    <category term="インターン2022" scheme="https://future-architect.github.io/tags/%E3%82%A4%E3%83%B3%E3%82%BF%E3%83%BC%E3%83%B32022/"/>
  </entry>
</feed>
