【Kiro活用】シェル×COBOLの古いバッチ資源を自動でドキュメント化するSkillを作った話|Steering×Skillsの実践例

AI & Next Tech
スポンサーリンク
スポンサーリンク

はじめに

シェルスクリプトがCOBOLやSQLを呼び出し、そのシェルがさらに別のシェルを呼ぶ——。長年運用されている基幹システムのバッチ処理には、こういう「呼び出し階層が頭の中にしかない」資源がよくあります。障害調査のたびに、シェルを1本ずつ開いて手でたどる作業に時間を取られた経験がある方も多いのではないでしょうか。

今回は、AWS Kiroの Steering(プロジェクトの前提知識をAIに教える仕組み)と Skills(繰り返し使う手順をコマンド化する仕組み)を組み合わせて、バッチの呼び出し階層を自動でドキュメント化する仕組みを作ってみました。本記事では、架空のバッチ構成を題材に、設計のポイントと実装でつまずいた箇所を紹介します。

Kiroの基本機能(Spec・Vibe・Steering・Hook・Skills)そのものの説明は、以前書いた AWS Kiroの使い方完全解説 にまとめているので、今回は「Steering×Skillsを使って実際に1本の仕組みを作る」応用編として読んでもらえればと思います。

スポンサーリンク

この記事で分かること

  • 古いバッチ資源(シェル+COBOL+SQL)の呼び出し階層を、Kiroでどう自動ドキュメント化するか
  • Steeringに「前提知識」、Skillsに「作業手順」を分けて書く設計の考え方
  • 正規表現でシェルスクリプトを解析する際につまずいたポイントと、実際に動くPowerShellコード(環境分岐・変数経由の呼び出し・$matchesの上書き事故)
  • 「全部自動」と「重要な部分だけ人が加筆」を使い分ける2段構成の作り方
スポンサーリンク

題材:架空の受注出荷バッチシステム

実在のシステムではなく、説明用に簡略化した架空の構成です。よくある「シェルがCOBOL/SQLを呼び、シェルの中からさらに別のシェルを呼ぶ」バッチシステムを想定しています。

batch-sample/
├── shell/              # 起点ジョブ+業務シェル(.sh)
├── cobol/              # COBOLソース(.cbl)
├── sql/                 # SQL(.sql)・SQL*Loader制御(.ctl)
└── docs/calltree/        # 生成される呼び出し階層ドキュメントの出力先

資源の命名規則も、サンプルとして次のように決めました。

  • 基本形:{ブロック2文字}{種別1文字}{連番3桁}
  • ブロック:OR受注/SH出荷/IV在庫/BL請求/CM共通
  • 種別:Sシェル/QSQL/CSQL*Loader制御

例えば ORS010 は「受注ブロックのシェル、010番」という意味になります。このルールをAIに一度教えておけば、以降の解析で資源名から業務領域を推測できるようになります。

Steeringに「前提知識」を書く

KiroのSteeringは、プロジェクトに関する前提知識をAIに常に参照させる仕組みです。今回は、リポジトリ構成・命名規則・呼び出し抽出パターンを.kiro/steering/batch-overview.mdにまとめました。

# バッチ資源 全体構成

## リポジトリ構成
(上記のディレクトリ構成を記載)

## 命名規則
(ブロック・種別の対応表を記載)

## 呼び出し構造の抽出パターン
| 呼び出し種別 | シェル上の記法 | 補足 |
|---|---|---|
| 共通env | 行頭 `. ` に続く `*.sh` | CMS000.sh が典型 |
| 子シェル | `$SHELLDIR/XXX.sh` | 変数経由の場合もある |
| COBOL | `$BINDIR/XXX` | 変数の場合は同シェル内の代入を追う |
| SQL | `sqlplus` 行の `@XXX.sql` | |
| SQL*Loader | `sqlldr` 行の `XXX.ctl` | |

ポイントは、「何を・どこで・どう書くか」という前提知識と、「どう作業するか」という手順を分けて書くことです。前提知識はSteeringに、作業手順は次に説明するSkillsに置きます。こうしておくと、資源の命名規則が変わったときもSteeringだけ直せばよく、Skillsの手順自体は変更不要になります。

Skillsに「作業手順」を書く

Skillsは、繰り返し使う作業をコマンド化する仕組みです。.kiro/skills/batch-calltree/SKILL.mdに、コールツリードキュメントの生成手順を定義しました。

---
name: batch-calltree
description: シェル/COBOL/SQLのバッチ資源を解析し、呼び出し階層ドキュメントをdocs/calltree/に生成する
---

# batch-calltree: バッチ呼び出し階層ドキュメント生成Skill

## 成果物の仕様
- 配置: docs/calltree/
- ファイル名: {シェル名}.md
- 本文: ヘッダ(メタ情報)+凡例+ネストリストの呼び出し階層

## 作成モードの選択
- 自動モード(既定): scripts/calltree_engine.ps1 を実行するだけ
- 手加筆モード(重要バッチのみ): 自動出力を土台に、業務的な意味を人が加筆する

詳細な出力フォーマット(種別ラベルの意味、リンクの書き方など)はreferences/format.mdに、バッチ構造のパターン集はreferences/patterns.mdに分けました。SKILL.md本体を短く保ち、詳細は参照ファイルに逃がすのがコツです。

自動モードの出力イメージ

calltree_engine.ps1は、シェルスクリプトを正規表現で解析し、呼び出し階層をMarkdownに変換します。解析ロジックの骨格は次のようになります(本記事用に簡略化していますが、実際に動かして下記の出力を確認済みのコードです)。

function Classify-Line([string]$line, [hashtable]$vars) {
    # 子シェル呼び出し(変数経由): . $SHELLDIR/$BIN_NAME
    if ($line -match '^\.\s+\$\{?SHELLDIR\}?/\$\{?([A-Za-z0-9_]+)\}?\s*$') {
        $varName = $matches[1]
        $resolved = if ($vars.ContainsKey($varName)) { $vars[$varName] } else { '$' + $varName }
        $kind = if ($resolved -match '^CMS000') { 'ENV' } else { 'SHELL' }
        return @{ Kind = $kind; Name = $resolved }
    }
    # 子シェル呼び出し(直接指定): . $SHELLDIR/CMS000.sh
    if ($line -match '^\.\s+\$\{?SHELLDIR\}?/([A-Za-z0-9_]+\.sh)\s*$') {
        $name = $matches[1]
        $kind = if ($name -match '^CMS000') { 'ENV' } else { 'SHELL' }
        return @{ Kind = $kind; Name = $name }
    }
    # COBOL呼び出し: $BINDIR/ORB010
    if ($line -match '^\$\{?BINDIR\}?/([A-Za-z0-9_]+)\s*$') {
        return @{ Kind = 'COBOL'; Name = $matches[1] }
    }
    # SQL呼び出し: sqlplus ... @ORQ010.sql
    if ($line -match 'sqlplus' -and $line -match '@\S*?([A-Za-z0-9_]+\.sql)\b') {
        return @{ Kind = 'SQL'; Name = $matches[1] }
    }
    return $null
}

先ほどの命名規則に沿って、架空の「受注集計バッチ(ORS010)」のシェルスクリプトをこのロジックに実際に読ませると、次のようなドキュメントが生成されます。

# ORS010.sh 受注集計バッチ

| 項目 | 値 |
|---|---|
| ジョブID | ORJ010 |
| 機能概要 | 当日受注データを集計し、出荷予定データへ連携する |

## 呼び出し階層

- ORS010.sh 受注集計バッチ ( shell/ORS010.sh )
  - [ENV] CMS000.sh 共通環境変数の読み込み ( shell/CMS000.sh )
  - [STEP1 受注データ抽出]
    - [COBOL] ORB010 当日受注データをワークテーブルへ抽出 ( cobol/ORB010.cbl )
  - [STEP2 在庫引当判定]
    - [COBOL] IVB020 在庫引当可否を判定 ( cobol/IVB020.cbl )
    - [条件] IVB020の戻り値が引当不可(RC=51)のとき
      - [COBOL] CMB030 エラーログ出力 ( cobol/CMB030.cbl )
  - [STEP3 集計テーブル更新]
    - [SQL] ORQ010.sql 受注集計テーブルを更新 ( sql/ORQ010.sql )
  - [SHELL] SHS010.sh 出荷予定データ連携 → 詳細は SHS010.md 参照
  - [COBOL] CMB900 バッチ終了ログ出力 ( cobol/CMB900.cbl )

STEPごとに見出しを立て、条件分岐や子シェルへの委譲が一目でわかる形にしています。子シェル(SHS010.sh)は中身を展開せず、別ドキュメントへのリンクに留めているのもポイントです。同じ子シェルを複数の親バッチが呼んでいても、ドキュメントが二重管理にならずに済みます。

実装でつまずいたポイント

1. 環境分岐を平坦に並べてしまう

最初に作ったエンジンは、if〜elifの分岐をそのまま上から順に全部拾ってしまい、「本番環境の設定を読み込んだあと、続けて開発環境の設定も読み込む」ように見えるドキュメントになってしまいました。

if   [ $ENV_NAME = PRD ]; then . shell/CMS000_prd.sh
elif [ $ENV_NAME = DEV ]; then . shell/CMS000_dev.sh
fi

実際には実行時にどちらか1系統しか通らないので、「分岐」であることを明示する表現に直しました。

- [分岐] 実行環境により下記いずれか1系統を実行
  - 本番(PRD): [ENV] CMS000_prd.sh
  - 開発(DEV): [ENV] CMS000_dev.sh

実装では、if〜fiの範囲をひとつのブロックとして読み進め、各枝の呼び出し行だけをClassify-Lineに渡して集約する形にしました。

if ($s -match '^if\s+\[\s*\$ENV_NAME') {
    $branches = New-Object System.Collections.Generic.List[string]
    $j = $i
    while ($j -lt $lines.Count) {
        $bs = $lines[$j].Trim()
        if ($bs -match '^(?:if|elif)\s+\[\s*\$ENV_NAME\s*=\s*(\w+)\s*\]\s*;\s*then\s+(.+)$') {
            $env = $matches[1]
            $call = Classify-Line $matches[2] $vars
            if ($call -ne $null) { $branches.Add("${env}: $($call.Kind) $($call.Name)") }
        }
        if ($bs -match '^\s*fi\s*$') { break }  # fiまで読んだらブロック終了
        $j++
    }
    # $branchesを1つの「分岐」ノードとしてまとめて登録する
}

2. 変数経由の子シェル呼び出しを拾えない

シェルの中には、呼び出すシェル名を一度変数に代入してから実行するパターンがあります。

BIN_NAME=SHS010.sh
. $SHELLDIR/$BIN_NAME

素朴な正規表現では$SHELLDIR/$BIN_NAMEの$BIN_NAMEが何を指すか分からず、呼び出しを見落としていました。同一シェル内の変数代入(BIN_NAME=SHS010.sh)を先に拾っておき、呼び出し箇所で変数名を実際の値に置き換えてから判定する、という2段階の処理に直して解決しました。

# 1段目: 変数代入行を先に拾って$varsに記録しておく
if ($s -match '^([A-Z_][A-Z0-9_]*)=([A-Za-z0-9_.]+)\s*$') {
    $vars[$matches[1]] = $matches[2]
}

# 2段目: 呼び出し側で変数名を実際の値に置き換えてから種別判定する
if ($line -match '^\.\s+\$\{?SHELLDIR\}?/\$\{?([A-Za-z0-9_]+)\}?\s*$') {
    $varName = $matches[1]
    $resolved = if ($vars.ContainsKey($varName)) { $vars[$varName] } else { '$' + $varName }
    # ここで$resolvedを使って種別(ENV/SHELL)を判定する
}

3. 共通処理が何度も出て冗長になる

CMB900(終了ログ出力)のような共通処理は、ほぼ全てのバッチの末尾に登場します。全バッチのドキュメントに律儀に書くと、本当に見たいステップの情報が埋もれてしまいます。重複する共通処理は1回だけ表示し、同じ処理が繰り返される旨を注記する形に落ち着きました。

4. -match演算子が$matchesを上書きする

呼び出し名を取得した直後に、その名前の種別(ENV/SHELLなど)を判定するためもう一度-matchを使ったところ、取得済みの名前が消えるという事故に遭遇しました。

# NG: 種別判定の-matchで$matchesが上書きされ、Nameが空になる
if ($line -match '^\.\s+\$\{?SHELLDIR\}?/([A-Za-z0-9_]+\.sh)\s*$') {
    $kind = if ($matches[1] -match '^CMS000') { 'ENV' } else { 'SHELL' }
    return @{ Kind = $kind; Name = $matches[1] }  # ← 既に別の$matchesに置き換わっている
}
# OK: 名前を先に変数へ退避してから、2回目の-matchを行う
if ($line -match '^\.\s+\$\{?SHELLDIR\}?/([A-Za-z0-9_]+\.sh)\s*$') {
    $name = $matches[1]
    $kind = if ($name -match '^CMS000') { 'ENV' } else { 'SHELL' }
    return @{ Kind = $kind; Name = $name }
}

$matchesはPowerShellの組み込み変数で、-matchを使うたびにグローバルに上書きされます。1つの処理の中で-matchを複数回使うときは、必要な値を先に変数へ退避してから次の判定に進むのが安全です。

自動モードと手加筆モードの使い分け

エンジンによる自動生成は「構造を機械的に正しく拾う」ことには強い一方、「なぜこの条件分岐があるのか」「障害調査で最初に疑うべき箇所はどこか」といった業務的な意味づけまではできません。

そこで、障害調査の対象になりやすい重要バッチだけ、自動出力を土台に人が加筆する手加筆モードを用意しました。

  • 自動モード:すべてのバッチに適用。構造を機械的に正しく拾う
  • 手加筆モード:基幹バッチ・複雑な制御(リラン/リトライ)を持つバッチのみ。自動出力に業務知識を加筆する

加筆したバッチは、再生成のたびに上書きされないよう、保護リストに登録しておきます。この仕組みにより、「ドキュメントは全件あるが、重要な部分は人の目で意味づけされている」という状態を保てます。

まとめ

  • KiroのSteeringには「前提知識」(構成・命名規則・抽出パターン)、Skillsには「作業手順」を分けて書くと、ルール変更に強い構成になる
  • シェルスクリプトを正規表現で解析する際は、環境分岐の集約・変数経由の呼び出し解決・共通処理の重複排除・$matchesの上書き事故が実務上のつまずきポイントになりやすい
  • 「全部自動生成」と「重要な部分だけ人が加筆」を分けると、網羅性と精度を両立できる

今回のような「レガシーな資源を機械的に解析してドキュメント化する」用途は、Kiroに限らずAIコーディングエージェント全般に向いている作業だと感じました。同じようなバッチ資源を抱えている方の参考になれば幸いです。

コメント