Excel方眼紙をKiroに読ませてAmazon Redshiftの仕様駆動開発を成立させるためにやったこと

記事タイトルとURLをコピーする

さとうです。

以前、下記で社内勉強会の資料を公開しました。

社内で「低級言語が高級言語に置き換わったのと同じように自然言語が開発言語になる時代になった」など大局的な視点で時流を感じさせる議論の種になったり、あまり面識のなかった人がこの資料の感想を話してくれるなど色々な意味で反響のあった資料でした。

blog.serverworks.co.jp

仕様駆動開発のアプローチで巨大なPL/pgSQLを開発したという話なのですが、わかりやすさ重視で詳細は割愛しています。

実際にやっていた内容はExcelの仕様書をKiroのSpecに落とし込んで仕様駆動開発をするというものでした。

様々な事情がありExcelでドキュメントを管理している方は少なくないと思います。

この記事では実際にKiroで仕様駆動開発をする際に実践していたことをもう少し詳細に書いていきます。

Excel方眼紙をKiroにどう理解させたか

Excelで仕様駆動開発をする際の課題

Kiroもそうですが、一般的に仕様駆動開発はAIにコンテキストとして理解させるために仕様書はMarkdownで書かれていることが前提となっています。

Excelで仕様駆動開発したい場合、Markdownの違いはシンプルに言うと「バイナリかテキストか」という点に集約されると思います。Excelはバイナリであり、アプリケーションに依存した固有フォーマットであることから汎用的なテキストベースの変更管理を難しくしています。

これは仕様駆動開発において重要となるGitを使用した変更管理やSpecと実装の同期が取りにくいことを意味しています。

ではExcelをMarkdownに変換する方法はというと、GeminiなどのOCRを搭載したサービスにpdfを読ませたりClaude in Excelを使って生成する方法が考えられると思います。

しかしこういった手法は変換プロセスが属人的で処理内容がブラックボックスであり、1回限りの生成であれば有用であるものの、継続的な変更管理を行うための手法としては相性が悪いものになります。ハルシネーションのチェックも必要でしょう。

仕様書は通常複数回の変更を重ねて確定させていくものなので、都度AI駆動で変更を反映するには差分検知の仕組みが不可欠です。

誰もが一度は見たことがあるExcelの変更履歴

ですので、原則としてExcelの変換ロジックは冪等性を担保して継続的に変更管理ができるようにプロセスを組み立てることが重要と考えています。

スクリプトで冪等になるように加工する

例として以下のようにスクリプトで、シートごとにセルに含まれている文字列を機械的に抽出して出力すると処理を冪等にでき、差分検知を容易にすることができます。

ドキュメントをExcelで書く場合、所謂Excel方眼紙になっていることが多いと思います。

Excelの座標関係をJSONで表現することで、AIに読ませる際に方眼紙で書かれた不規則な表構造などの推論精度を上げることが期待されます。

Python版

import sys
import json
import openpyxl
from datetime import datetime, date

def excel_to_json(file_path):
    wb = openpyxl.load_workbook(file_path, data_only=False)
    
    result = {}
    for sheet_name in wb.sheetnames:
        ws = wb[sheet_name]
        sheet_data = []
        
        for row in ws.iter_rows():
            row_data = {}
            for cell in row:
                if cell.value is None:
                    continue
                
                col_letter = cell.column_letter
                value = cell.value
                
                # datetime型は文字列に変換
                if isinstance(value, (datetime, date)):
                    row_data[col_letter] = value.isoformat()
                else:
                    row_data[col_letter] = value
            
            if row_data:  # 空行はスキップ
                sheet_data.append(row_data)
        
        result[sheet_name] = sheet_data
    
    return json.dumps(result, ensure_ascii=False, indent=2)

if __name__ == "__main__":
    if len(sys.argv) < 2:
        print("使用方法: python excel_parser.py <Excelファイルパス>")
        sys.exit(1)
    
    file_path = sys.argv[1]
    
    try:
        result = excel_to_json(file_path)
        print(result)
    except Exception as e:
        print(f"エラー: {e}", file=sys.stderr)
        sys.exit(1)

PowerShell版(COMオブジェクトを使ったモジュール不要版。処理は遅い)

param(
    [Parameter(Mandatory=$true)]
    [string]$FilePath
)

$excel = $null
try {
    # 要Excelアプリケーション
    $FilePath = Resolve-Path $FilePath
    $excel = New-Object -ComObject Excel.Application
    $excel.Visible = $false
    $excel.DisplayAlerts = $false
    
    $workbook = $excel.Workbooks.Open($FilePath)
    $result = @{}
    
    foreach ($sheet in $workbook.Worksheets) {
        $sheetData = @()
        $usedRange = $sheet.UsedRange
        
        if ($null -ne $usedRange) {
            for ($row = 1; $row -le $usedRange.Rows.Count; $row++) {
                $rowData = @{}
                
                for ($col = 1; $col -le $usedRange.Columns.Count; $col++) {
                    $cell = $usedRange.Cells.Item($row, $col)
                    $value = $cell.Value2
                    
                    if ($null -ne $value) {
                        $colLetter = ""
                        $temp = $col
                        while ($temp -gt 0) {
                            $temp--
                            $colLetter = [char]([int](65 + ($temp % 26))) + $colLetter
                            $temp = [math]::Floor($temp / 26)
                        }
                        
                        if ($value -is [DateTime]) {
                            $rowData[$colLetter] = $value.ToString("o")
                        } else {
                            $rowData[$colLetter] = $value
                        }
                    }
                }
                
                if ($rowData.Count -gt 0) {
                    $sheetData += $rowData
                }
            }
        }
        
        $result[$sheet.Name] = $sheetData
    }
    
    $workbook.Close($false)
    $result | ConvertTo-Json -Depth 10 -Compress:$false
    
} catch {
    Write-Error "エラー: $_"
    exit 1
} finally {
    if ($null -ne $excel) {
        $excel.Quit()
        [System.Runtime.Interopservices.Marshal]::ReleaseComObject($excel) | Out-Null
    }
}

使い方

$ python excel_parser.py <Excelのパス>

アウトプットの例

以下のようにシート別にセルの文字列をJSONでエクスポートします。

{
    "システム要件定義書": [
        {
            "A": "システム要件定義書"
        },
        {
            "B": "ここにシステム要件を記載"
        }
    ],
    "変更履歴": [
        {
            "A": "変更履歴"
        },
        {
            "C": "初版作成",
            "A": "v1.0"
        }
    ]
}

仕様は必ずしもMarkdownである必要はない

Kiroの場合.kiro/specs以下に機能別にディレクトリを作成し、requirements.mdに記述したものが仕様として認識されます。

このrequirementsは必ずしもMarkdownである必要はなく、前述のスクリプトから出力されるJSONを読み込ませるだけでも文脈理解には十分な精度を発揮しました。

(requirementsと認識させるために拡張子は.mdですが、中身はJSONです)

python tools/excel_parser.py "excel/stats01.xlsx"  > .kiro/specs/stats01/requirements.md

当初は以下のようにAgent Hooksを使い、JSON→Markdownへの変換を自動化しようとしましたが以下のように変換結果が安定しないという課題が残りました。

変更管理においてハルシネーションは致命的な問題となるため、LLMありきの仕様管理の仕組みは組まないことを推奨します。

  • Markdownの出力形式が安定しない
  • コミットログから差分のみ反映するように指示しても、反映漏れが発生する
# 役割
あなたはKiroのAgentとして、トリガーされたJSONファイル(Excelから変換されたデータ)をもとに、仕様(Spec)の作成および`requirements.txt`の更新を行います。

# 入力データ
- トリガーされたファイル: JSON形式(各シート名をキーとし、列名と値のペアを持つ行データのリスト)
- メタデータ: 現在のJSONのCommit Id

# 処理ステップ
以下のステップに沿って処理を実行してください。

## Step 1: Specの存在確認
保存されたファイルと同名の仕様(Spec)が存在するか確認してください。
- 存在しない場合:
  - 「JSONと同じ名前のSpecを作成してから再度JSONを保存してトリガーさせてください」とメッセージを出力して処理を終了します。
  - Specは作成しないでください。
- 存在する場合:
  - Step 2へ進みます

## Step 2: JSONのCommit Idの検証
トリガーされた現在のJSONデータに「Commit Id」が存在するか確認してください。
- 存在しない場合: 「JSONをCommitしてから再度実行してください」とメッセージを出力して処理を終了します。
- 存在する場合: 現在のJSONのCommit Idを変数として保持し、Step 3へ進みます。

## Step 3: requirements.txtの確認と分岐
現在のSpec内の`requirements.txt`の先頭行を確認し、記載されている「過去のCommit Id」を読み取ります。

### パターンA: 過去のCommit Idが存在しない(または空白の)場合【新規作成】
1. 以下の【出力フォーマット】に従い、JSONの構造を解析してMarkdownに変換し、`requirements.txt`に記述します。
2. 元のフォーマットやデータ内容は一切変更しないでください。セル番号から空間情報を再構成し、JSONに含まれた全ての文字列をMarkdownに表現してください。
3. 処理を完了して終了します。

### パターンB: 過去のCommit Idが存在する場合【更新】
1. 「現在のJSONのCommit Id」と「`requirements.txt`に記載された過去のCommit Id」を比較します。
2. 両者が同一の場合: 「変更がありません。JSONをCommitしてから実行してください」と出力して処理を終了します。
3. 両者が異なる場合: 変更差分を特定し、`requirements.txt`の内容を最新のJSONデータに合わせて更新します。
    変更差分を漏れなく反映し、変更差分以外は変更しないでください。
4. その際、`requirements.txt`の先頭にあるCommit Idも「現在のJSONのCommit Id」に書き換えてください。
---
# 出力フォーマット(Markdown変換ルール)
`requirements.txt`を新規作成または更新する際は、必ず以下の構造に従ってください。

Commit Id: [ここに現在のJSONのCommit Idを記載]

# [シート名 1]
## [シート内容のベースとなる項目名などを記載]
- [列名A]: [値]
- [列名B]: [値]

# [シート名 2]
...

図類の管理について

DFDやE-R図などの図類は画像ファイルで推論させる方法もありますが、ExcelでMermaidやdraw.ioのテキストをシートで管理するというテクニックも有効です。

以下の記事で紹介したようにdraw.ioで図類を作成すると内部ではXMLで作成されるため、テキスト同様に変更管理も可能となります。

blog.serverworks.co.jp

ただし、draw.ioの中に外部から取得した画像を埋め込むとバイナリとして扱われる点には注意してください。

コンテキスト管理の重要性

上記でExcelを仕様書としてKiroに理解させることができることはわかりました。

一方で、仕様書だけでは解決できない問題もあります。

RedshiftなどのDWHで実行される分析クエリを仕様駆動開発で開発する際の固有の課題として、コンテキスト管理の重要性についても触れておきたいと思います。

分析クエリは多大なコンテキストが必要

一般的なフレームワークを利用したアプリ開発と比べると、分析クエリは非常に多くのコンテキストを必要とします。

通常のアプリ開発であれば「〇〇機能を〇〇(フレームワーク)で作って」と指示するだけで、フレームワークに関する実装は世界共通のものでLLMが事前学習によって理解しているため仕様だけでも精度の高い実装が可能です。

一方で分析クエリの場合、独自に作成したテーブルのスキーマやデータなどLLMが学習していない内容も明示的にコンテキストとして指示する必要があります。トランザクションデータの他にマスタデータ、それらの依存先のテーブルなど具体度の高い入力が必要になります。そこで問題になるのがコンテキスト管理です。

LLMにはコンテキストウィンドウというワーキングメモリが存在しており、この情報を元にして推論が行われます。

直近ではClaude Opus 4.6が従来の5倍となる100万トークンのコンテキストウィンドウをサポートするなど飛躍的な進化を遂げていますが、それでも数億規模のレコードを保持することは現実的ではないでしょう。

blog.serverworks.co.jp

コンテキスト管理のポイント

関連するテーブルの定義(DDL)をインプットとして与えることも有効ですが、それだけでは不十分です。スキーマと整合するだけの実態と乖離したデータが生成されてしまうためです。

Redshiftのような分散型DWHでは、分散キーやソートキー、データのカーディナリティ、NULLの割合といった物理データの状態を反映したクエリ実装が重要になります。

これら膨大なメタデータや業務仕様をすべてプロンプトに詰め込むことは、コンテキストウィンドウの制限からも現実的ではありません。そこで重要になるのが、AIに必要な時だけ必要な情報を引き出させる「動的なコンテキスト管理」です。

AWS MCP Serverを介したRedshiftへのアドホッククエリ環境の提供

事前にすべての情報をプロンプトに埋め込むのではなく、コンテキストをAIエージェントがセルフサービスで読みにいけるようにMCP Serverを活用します。

具体的には、MCP Serverを介してAI自身が SVV_TABLE_INFO などのシステムテーブルにアドホッククエリを投げ、実際のレコード数、データの偏りなどをを把握します。これにより、非効率な処理や仕様書の意図とデータの食い違いなどを検知することができます。

冒頭で触れた勉強資料からの引用

2026/03時点ではPreviewですが、AWS MCP Serverというサービスを使うとフルマネージドなMCP Serverを利用することができます。

blog.serverworks.co.jp

Preview版はus-east-1のみ対応している状況なので、下記のローカル版のAWS API MCP ServerやAWS Knowledge MCP Serverの利用も検討してください。

(AWS MCP Serverはローカル版を段階的に統合していく計画のようです)

AWS API MCP Server | Welcome to Open Source MCP Servers for AWS

AWS Knowledge MCP Server | Welcome to Open Source MCP Servers for AWS

データリネージを考慮したSpecの整理

データの加工処理の依存関係を示したものをデータリネージと呼びます。

分析クエリの開発は「上流の生データ(データレイク) → 統合データ(DWH) → 分析用データ(データマート)」という連鎖の上に成り立っていることが大半です。

例えばテーブルCテーブルAテーブルBを元にして作られる場合、テーブルCテーブルAテーブルBのスキーマやクエリを正しく理解していることが前提になります。

そのため、仕様駆動開発をする際もこの依存関係を考慮して機能別に仕様を整理することが重要です。

Kiroのプロジェクトの例

上記をふまえると、最終的には以下のようなワークフローになりました。

また、KiroのSpecの構成は以下のようになりました。

.kiro
├── hooks
│   └── excel-to-md.kiro.hook
├── specs
│   ├── stats01 # 依存関係なし
│   │   ├── design.md # 中身はJSON
│   │   ├── requirements.md
│   │   └── tasks.md
│   ├── stats02 # 依存関係なし
│   │   ├── design.md # 中身はJSON
│   │   ├── requirements.md
│   │   └── tasks.md
│   └── stats03 # stats01とstats02に依存
│       ├── design.md # 中身はJSON
│       ├── requirements.md
│       └── tasks.md
└── steering
    └── system_design.json # システム要件定義書 中身はJSON
    └── test_plan.json # テスト計画書 中身はJSON
    └── tables.md # Redshiftのスキーマおよびテーブル名の情報
    └── coding_rules.md # コーディング規約や例外処理の基本方針など

steeringでシステム要件定義書、テスト計画書など実装の前提となるドキュメント(Excel)をJSONで解釈させることでテストの精度(テスト観点、tasksの粒度、非機能面のテストケース策定など)を上げることができます。

design.mdの元となったExcelには以下のような実装固有の情報を含めます。テーブルの中身は基本的にMCP Server経由で自律的に見に行く方式とするため、steeringでテーブルやスキーマの物理名がわかるように明示しておきます。

  • 仕様書(使用するテーブルの種類やPL/pgSQLの入出力を明記)
  • DFDをMermaidにしたテキスト(AIで作成)
  • DDLのSQL

画像はSTATS01という仮名で実際に作成されたdesignとtaskの一部ですが、テストプランを明示することでテストデータの考慮や、CTE単位のテストクエリの作成など現実のデータと乖離のない実装計画を作成することができます。

design.mdの一部

tasks.mdの一部

Kiroを安全に使うための権限統制

AIエージェントに自律的にコンテキストを理解させる方法を説明しましたが、手放しでAIエージェントに本番環境を操作させることは非常に危険です。テストデータを勝手に挿入されてしまったり消去されては目も当てられない事態になります。

そのため、権限統制は必ず考慮しておくべきトピックです。

IAMの権限統制

ReadOnlyをベース、書き込み権限はAssumeRoleでアドオン

sudo的な思想で、基本的にはAssumeRoleしかできない踏み台ユーザを作成して、ユースケース別に権限を付与したIAMロールを作成して都度AssumeRoleさせる運用が安全です。

例えばRedshiftで読み取りクエリを発行して実装計画を策定するタスクにおいては、RedshiftのPRIVILEGES権限やAWSリソースのPowerUser権限は明らかに不要です。

AssumeRoleで書き込み権限を引き受けられるようにAWS CLIを設定することで、簡単にプロファイル別で権限を使い分けられるようになります。

blog.serverworks.co.jp

AWS CLIのプロファイルの設定例は以下の通りです。

puというプロファイルを使うことで内部的にはstepというプロファイルからpuにAssumeRole(スイッチロール)するような挙動になります。

.aws/credentials

[step]
aws_access_key_id = <踏み台のアクセスキー>
aws_secret_access_key = <踏み台のシークレットアクセスキー>

.aws/config

[profile pu]
role_arn = arn:aws:iam::<アカウントID>:role/<AssumeRoleするIAMロール名>
source_profile = step
region = ap-northeast-1

カスタムエージェントによる権限の使い分け

Kiro CLI限定の機能ですが、カスタムエージェントを使うと上記のプロファイルの使い分けが容易になります。

kiro.dev

上記で作成したAssumeRole用のプロファイルを使うように明示的な指示を入れたエージェントを作成しておくことで、使わせたい権限をエージェントを切り替えるだけで明示的に指定することができます。

KiroのIDEから呼び出すことができないため、task作成までをKiro IDEでやり切った上でKiro CLIに渡すなどの工夫は必要になります。

以下のようにKiro CLIを起動した状態でコマンドからカスタムエージェントを作成することができます。

/agent create --name <エージェント名>

上記のコマンドを実行するとカスタムエージェントの編集画面になるのでプロンプトで利用可能なプロファイルを指示します。

作り込みとしてはシンプルで、プロンプトで利用可能なAWS CLIのプロファイルを明示するだけです。

{
  "name": "pu-agent",
  "description": "",
  "prompt": "あなたはAWS環境のパワーユーザーエージェントであり、AWS操作には必ず`pu`プロファイルを使用します。それ以外のプロファイルは使用しないでください。",
  "mcpServers": {},
  "tools": [
    "*"
  ],
  "toolAliases": {},
  "allowedTools": [],
  "resources": [
    "file://AGENTS.md",
    "file://README.md"
  ],
  "hooks": {},
  "toolsSettings": {},
  "useLegacyMcpJson": true,
  "model": null
}

/agent swapで作成したカスタムエージェントに切り替えると、スイッチロールのようにプロファイルを直感的に使い分けることができます。

> /agent swap

✔ Choose one of the following agents · pu-agent


[pu-agent] > S3バケット一覧をください。

> S3バケットの一覧を取得します。
Running aws cli command (using tool: aws):

Service name: s3api
Operation name: list-buckets
Parameters:
Profile name: pu
Region: ap-northeast-1
Label: S3バケット一覧の取得
Allow this action? Use 't' to trust (always allow) this tool for the session. [y/n/t]:

[pu-agent] > y

 - Completed in 1.791s

> S3バケット一覧:
...(中略)...

Redshiftの権限統制

テストは一時テーブルで完結させる

テストデータの挿入やクエリ条件の妥当性チェックなどの単体テストレベルの検証であれば、一時テーブルで完結させるように計画に明示することを推奨します。

Redshiftの場合、CREATE (TEMP|TEMPORARY) TABLEとすると一時スキーマのpg_tempにテーブルが作成され、セッション終了時に自動削除されるようになります。

CREATE TEMP TABLE IF NOT EXISTS XXXX (
    XXX INTEGER NOT NULL,
    XXX VARCHAR(128),
    XXX VARCHAR(128),
    XXX VARCHAR(128),
    XXX DOUBLE PRECISION NOT NULL
);

docs.aws.amazon.com

原則SELECT権限以上は与えない

原則はpg_tempに対する書き込み権限とデータソースとなるテーブルに対するSELECT権限のみを付与します。

なお、先ほど紹介したMCP Serverを経由して接続する場合にはReadOnlyToolのみをIAMポリシーで許可するなど、IAMベースで権限のガードレールを設定することができます。

※2026/03時点でAmazon Redshift MCP Serverには読み込み系のToolしかありませんが、近日中にWrite系のToolを実装予定という趣旨の記載があり、ある日突然書き込みを始めてしまうかもしれません...

awslabs.github.io

SECURITY DEFINERで必要な書き込み権限のみを与える

本番データを使用した処理時間の検証など非機能要件に対するテストで、実際のテーブルにINSERTするなど書き込みの権限が必要になる場合があります。

このようなケースにおいてはストアドプロシージャ(PL/pgSQL)を所有者の権限で実行するSECURITY DEFINERの権限を付与することで、処理内の書き込み権限のみを許可するといった制御が可能になります。

このような方法でなるべく書き込み権限は最小レベルで付与するような工夫が重要になります。

docs.aws.amazon.com

注意喚起: 責任ある生成AIの利用を

勉強会資料からの再掲です。

本記事で紹介する内容を実際の開発に適用する場合、顧客やサービスオーナーなどのステークホルダーと合意を得ることが前提です。

生成AIの利用自体に対する許諾もそうですが、開発環境へのデータの持ち出しが発生する場合はその許可も必要になるでしょう。

所属会社のガイドラインや規約があれば必ず事前に確認するようにしましょう。

おわりに

記載する内容については私が経験した開発プロジェクトを踏まえて一般化したものであり、すべてのプロジェクトで成立することを約束するものではありません。ただ、変更管理の仕組みを確立すれば大規模な仕様駆動開発も不可能ではないと考えています。

冒頭に書いた通り仕様駆動開発においてはExcelを使わないに越したことはありませんが、開発上の制約をクリアする工夫の1つとしてどなたかの参考になれば幸いです。

佐藤 航太郎(執筆記事の一覧)

クロスインダストリー第1本部 クラウドモダナイズ課
最近はデータエンジニアのようなことをしています。