本記事は生成AIと共同で執筆しています。事実関係は可能な範囲で公式ドキュメント等と照合していますが、誤りが含まれている可能性があります。重要な判断を行う前にご自身でも一次情報をご確認ください。
前回の記事 で、組織向けに「非エンジニアが操作できる管理コンソール」をGitHub App + Cloudflare Accessで構築した話を書きました。本記事はその続編で、「サイトを増やしてもコードに触らずに済む」「1サイトに複数アクションを持たせる」 ために、設定をYAMLに外部化した設計を整理します。
当初の設計の限界
最初は src/lib/sites.ts に TypeScript の配列としてサイトを直書きしていました。
export const SITES = [
{
id: 'site-a',
url: 'https://site-a.example.com/',
action: { type: 'github-workflow', repo: '...', workflow: 'admin.yml' },
},
// ...
]
この設計には以下の問題がありました。
- サイトを追加するたびにコード変更が必要 — i18nテキスト用にmessages.jsonも編集
- 1サイト1アクションが前提になっていた — 「デプロイ」「再インデックス」「バックアップ」など複数を扱いたいときに表現できない
- OSSとして配布したときに利用者がコードを触らないと動かせない — 設定がコードに混在しているとフォークして編集する流れになり、本家の更新を取り込みにくい
YAML 1ファイルへの集約
これを config.yml 1ファイルへの集約に書き換えました。アプリのブランディング・サイト定義・アクション定義・各テキストのja/en、すべてここに入ります。
app:
title:
ja: 管理コンソール
en: Admin Console
homePage:
heading:
ja: 管理コンソール
en: Admin Console
lead:
ja: 各サイトのカードを開いて、操作を実行できます。
en: Open a site card to run operations.
sites:
- id: site-a
name:
ja: サイトA
en: Site A
description:
ja: サイトAの説明
en: Description
url: https://site-a.example.com/
actions:
- id: deploy
label:
ja: デプロイを実行
en: Deploy
description:
ja: ビルドして公開します。
en: Build and publish.
type: github-workflow
repo: your-org/site-a
workflow: deploy.yml
ref: main
- id: es-sync
label:
ja: ESインデックス更新
en: Re-index
description:
ja: Elasticsearchを再同期します。
en: Re-syncs Elasticsearch.
type: github-workflow
repo: your-org/site-a
workflow: es-sync.yml
ref: main
inputs:
- name: dry_run
type: boolean
default: false
label:
ja: Dry run
en: Dry run
利用者は このファイルだけ を編集すれば、コードを書かずに:
- 新しいサイトを追加できる
- 既存サイトに新しいアクションを追加できる
- ブランディングテキストや各サイトの説明文を自分の言葉に書き換えられる
- 多言語対応をja/en以外にも拡張できる(テンプレートを増やせば)
「設定→TypeScript型」への変換
サーバ側のコードは型付きTypeScriptで書きたいので、ビルド前にYAMLをパースして src/lib/sites.generated.ts を吐く小さなスクリプトを噛ませます。
// scripts/generate-config.js
const yaml = require('js-yaml')
const config = yaml.load(fs.readFileSync('config.yml', 'utf-8'))
const sitesTs = `// AUTO-GENERATED from config.yml. Do not edit by hand.
export type SiteAction =
| { id: string; type: 'github-workflow'; repo: string; workflow: string; ref: string; inputs?: ActionInput[] }
| { id: string; type: 'vercel-deploy-hook'; envHookKey: string }
export const SITES = ${JSON.stringify(normalizedSites, null, 2)}
export function getAction(siteId: string, actionId: string): SiteAction | undefined { ... }
`
fs.writeFileSync('src/lib/sites.generated.ts', sitesTs)
i18nテキストも同様に処理します。UI内部のテキスト(「読み込み中…」「実行履歴」など、利用者が普通は触らない部分)はテンプレート src/messages/_template/{ja,en}.json に置いておき、config.yml由来のテキスト(サイト名、アクションラベル等)と機械的にマージして src/messages/{ja,en}.json を生成します。
const merged = {
...template,
Common: {
title: config.app?.title?.[locale] ?? '',
},
Sites: Object.fromEntries(
config.sites.map((s) => [
s.id,
{
name: s.name?.[locale] ?? s.id,
description: s.description?.[locale] ?? '',
actions: Object.fromEntries(
s.actions.map((a) => [
a.id,
{ label: a.label?.[locale] ?? a.id, description: a.description?.[locale] ?? '' },
])
),
},
])
),
}
npm run dev / npm run build の前に npm run config:generate を自動で走らせるよう package.json に組み込んでおけば、YAML編集 → 即反映の流れになります。
1サイト複数アクションの UI
データ構造が actions[] の配列になったので、UI側もそれに合わせて再設計しました。
ホーム:サイトのカードを並べ、「詳細を開く」リンクのみ。実行ボタンは出さない。カード内ではそのサイトに紐づくアクション名だけバッジ表示します。
サイト詳細:上部に タブ を並べ、選択したアクションのパネルを表示。パネル内に:
- アクションの説明
- 入力(dry_runチェックボックス等)
- 実行ボタン
- 左側に直近20件の実行履歴
- 右側にジョブごとのリアルタイムログ
タブで切り替えるだけで、同じサイトの異なる種類の操作(デプロイ/インデックス更新/バックアップ等)を統一されたUIで操作できます。
設計の収穫
- 設定とコードの分離:保守する人が「YAMLだけ見れば良い」状態になり、フォーク後の追従が容易になる
- アクションの多様化に耐える:1サイトに何個でもアクションを足せる構造のまま、UIは破綻しない
- i18nの単一管理:表示文言の追加・変更がYAMLとtemplate JSONの2箇所に閉じる
- OSSとして公開できる形に近い:自組織以外でも
config.ymlを書き換えるだけで動く
残った課題と次のステップ
YAMLスキーマ検証
現状はYAMLの妥当性を実行時まで気づきません。Zod等でランタイム検証を入れるか、JSONSchemaを用意してエディタの補完・警告を効かせるのが望ましいです。
動的リロード
YAMLを編集してもデプロイし直さないと反映されません。Cloudflare KV等にYAMLを保存して動的に読み込む形に変えれば、ダッシュボード上から設定変更も可能ですが、複雑度とのトレードオフ。
環境ごとの上書き
ステージング環境では一部のリポジトリを差し替えたい、といったニーズには config.{env}.yml のオーバーライド機構が必要になります。今は単一YAMLのみ。
まとめ
「設定を編集するだけで動く」状態を成立させるためには、コードと設定を物理的に分離し、TypeScriptの型は生成物として扱うのが素直です。YAML → 型ファイル + JSONメッセージファイルの単純なジェネレータを1本書くだけで、利用者が触る面積は劇的に小さくなります。
OSSとして配布する管理ツールでは、最初から config.yml駆動 の前提で設計しておくと、フォーク後の保守コストが大きく下がります。あとから外部化するのは骨が折れるので、最初の段階でやっておくのが結局は楽でした。