cli-architecture-pattern について
インフラ操作を CLI ツールとして構成するためのリファレンススキルです。プロジェクトの複雑さに応じた1〜3層の設計を、JavaScript/TypeScript と Python の例とともに示します。
主な機能
- 011〜3層の CLI パターン
- 02プロジェクト規模に応じた設計選択
- 03引数解析とロジックの分離
- 04小さなコアモジュールの組み合わせ
- 05JavaScript/TypeScript と Python の例
活用シーン
- 01分散したインフラコマンドの整理
- 02プロジェクトの CLI 構成選択
- 03共有ロジックとエントリーポイントの分離
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ loxosceles-dev/ dev-skills --skill cli-architecture-patternインストール後、Claude Code で/cli-architecture-patternを入力して読み込まれたか確認します。
詳細
CLI アーキテクチャパターン
これは参考パターンです。 このアプローチから学び、自分のコンテキストに合わせて応用してください。そのままコピーしないでください。
問題:インフラ運用がシェルスクリプト、手動コマンド、デプロイパイプラインに分散すると、保守が難しくなり、一貫性も失われます。
解決策:統一されたエントリーポイントを備えた、構造化された CLI ツールです。
このパターンを採用する理由
利点:
- 一貫性:同じコマンドをローカル、CI/CD、デプロイスクリプトで使えます
- 可読性:
pnpm deploy:devは、AWS CLI の生のコマンドより明確です - テスト容易性:CLI コマンドを個別にテストできます
- 再利用性:異なるエントリーポイント間でロジックを共有できます
- ドキュメント:コマンドは
--helpで自己説明的になります
統合ポイント:
- ルートの
package.jsonスクリプト(JS/TS プロジェクト) pyproject.tomlスクリプト(Python プロジェクト)- GitHub Actions ワークフロー
- デプロイスクリプト(bash など)
- ローカル開発
統合例:
// package.json
{
"scripts": {
"deploy:dev": "ts-node cli/bin/deploy.ts --env=dev",
"export-params:dev": "ts-node cli/bin/ssm-params.ts export dev frontend"
}
}
# GitHub Actions
- name: Deploy infrastructure
run: pnpm deploy:dev
- name: Export parameters
run: pnpm export-params:dev
1 層アーキテクチャ(シンプルなプロジェクト向け)
構造:
lib/
infra.ts # Single entry point: arg parsing + business logic
core/
environment-manager.ts # Bootstrap config loader (shared utility)
単一ファイル - すべてのコマンドを 1 つの switch にまとめます:
// lib/infra.ts
import { EnvironmentManager } from './core/environment-manager';
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
import { CloudFormationClient, DescribeStacksCommand } from '@aws-sdk/client-cloudformation';
import { CloudFrontClient, CreateInvalidationCommand } from '@aws-sdk/client-cloudfront';
import { spawn } from 'child_process';
const [command] = process.argv.slice(2);
async function main() {
switch (command) {
case 'deploy':
await deploy();
break;
case 'publish':
await publish();
break;
case 'synth':
await run('npx', ['cdk', 'synth']);
break;
default:
console.log('Commands: deploy | publish | synth');
process.exit(1);
}
}
async function deploy() {
console.log('📦 Provisioning infrastructure...');
await run('npx', ['cdk', 'deploy', '--all', '--require-approval', 'never']);
console.log('🏗️ Publishing frontend...');
await publish();
}
async function publish() {
const stage = getStage();
const stackName = `myproject-web-${stage}`;
const region = new EnvironmentManager(stage).load().cdkDefaultRegion;
await run('pnpm', ['--filter', 'frontend', 'run', 'build']);
const bucketName = await getStackOutput(stackName, 'WebBucketName', region);
await syncToS3('frontend/out', bucketName, region);
const distId = await getStackOutput(stackName, 'CloudfrontDistributionId', region);
await invalidate(distId, region);
console.log('✅ Published');
}
// ... helper functions (run, getStackOutput, syncToS3, invalidate)
main().catch((err) => {
console.error(err.message);
process.exit(1);
});
統合:
// package.json
{
"scripts": {
"deploy:dev": "ENVIRONMENT=dev ts-node infrastructure/lib/infra.ts deploy",
"deploy:prod": "ENVIRONMENT=prod ts-node infrastructure/lib/infra.ts deploy",
"publish:dev": "ENVIRONMENT=dev ts-node infrastructure/lib/infra.ts publish",
"synth": "ENVIRONMENT=dev ts-node infrastructure/lib/infra.ts synth"
}
}
使用する状況:
- シンプルなプロジェクト(静的フロントエンド、単一スタック)
- コマンドが 5 個以下
- 抽象化する価値のある、コマンド間で共有されるロジックがない
- 迅速な実装を優先する
2 層への移行を検討する状況:
- コマンドの合計が約 200 行を超える
--verbose、--dry-runなどの共有フラグが必要になる- 複数のコマンドで大幅なロジックを共有する
2 層アーキテクチャ(ほとんどのプロジェクトに推奨)
構造:
cli/
bin/ # Tier 1: Argument parsing
commands/ # Tier 2: Business logic + SDK calls
shared/
env-loader.ts # Simple utilities
configs.ts
第 1 層:CLI バイナリ - エントリーポイントのみ
// bin/ssm-params.ts
import { Command } from 'commander';
import { getParameter } from '../commands/ssm-params';
const program = new Command();
program
.command('get <name>')
.action(async (name: string) => {
const value = await getParameter(name);
console.log(value);
});
program.parse();
第 2 層:コマンド - SDK を直接使用
// commands/ssm-params.ts
import { SSMClient, GetParameterCommand } from '@aws-sdk/client-ssm';
import { loadEnv } from '../shared/env-loader';
loadEnv();
const ssmClient = new SSMClient({
region: process.env.CDK_DEFAULT_REGION
});
export async function getParameter(name: string): Promise<string> {
const command = new GetParameterCommand({ Name: name });
const response = await ssmClient.send(command);
return response.Parameter?.Value ?? '';
}
使用する状況:
- 中規模のプロジェクト
- CLI ツールが 2~5 個
- 直感的な操作
- 迅速な開発を優先する
3 層アーキテクチャ(複雑なプロジェクト向け)
継承ではなくコンポジションを使用します。小さく、役割を絞ったモジュールをコマンドで組み合わせます。
構造:
cli/
bin/ # Tier 1: Argument parsing + error handling ONLY
commands/ # Tier 2: Business logic, composes core modules
core/
logger.ts # Tier 3: Standalone utilities
env.ts
aws/
ssm.ts
s3.ts
第 1 層:CLI バイナリ — エントリーポイントのみ(ビジネスロジックは含めない)
// bin/ssm-params.ts
import { Command } from 'commander';
import { getParameter, exportParams } from '../commands/ssm-params';
const program = new Command();
program
.command('get <name>')
.option('-v, --verbose', 'verbose output')
.action(async (name, opts) => {
const value = await getParameter(name, { verbose: opts.verbose });
console.log(value);
});
program
.command('export <stage> <service>')
.action(async (stage, service) => {
await exportParams(stage, service);
});
program.parse();
第 3 層:コアモジュール — 小さく、役割を絞り、継承を使わない
各モジュールは 1 つの処理を行い、通常の関数またはシンプルなファクトリー関数をエクスポートします:
// core/logger.ts
export function createLogger(verbose: boolean) {
return {
log: (msg: string) => { if (verbose) console.log(msg); },
error: (msg: string) => console.error(msg),
};
}
// core/env.ts
export function loadEnv(stage: string): Record<string, string> {
const envPath = `.env.${stage}`;
// Load from file, fall back to process.env
return { ...parseEnvFile(envPath), ...process.env };
}
export function requireEnv(key: string): string {
const value = process.env[key];
if (!value) throw new Error(`${key} is required`);
return value;
}
// core/aws/ssm.ts
import { SSMClient, GetParameterCommand, GetParametersByPathCommand } from '@aws-sdk/client-ssm';
export function createSSMClient(region: string) {
const client = new SSMClient({ region });
return {
async getParameter(name: string): Promise<string> {
const res = await client.send(new GetParameterCommand({ Name: name }));
return res.Parameter?.Value ?? '';
},
async getParametersByPath(path: string): Promise<Record<string, string>> {
const res = await client.send(new GetParametersByPathCommand({ Path: path }));
const params: Record<string, string> = {};
for (const p of res.Parameters ?? []) {
if (p.Name && p.Value) params[p.Name] = p.Value;
}
return params;
},
};
}
第 2 層:コマンド — コアモジュールを組み合わせ、すべてのビジネスロジックを含みます
// commands/ssm-params.ts
import { createLogger } from '../core/logger';
import { requireEnv } from '../core/env';
import { createSSMClient } from '../core/aws/ssm';
export async function getParameter(name: string, opts: { verbose?: boolean } = {}) {
const log = createLogger(opts.verbose ?? false);
const region = requireEnv('AWS_REGION');
const ssm = createSSMClient(region);
log.log(`Fetching parameter: ${name}`);
return ssm.getParameter(name);
}
export async function exportParams(stage: string, service: string) {
const region = requireEnv('AWS_REGION');
const projectId = requireEnv('PROJECT_ID');
const ssm = createSSMClient(region);
const params = await ssm.getParametersByPath(`/${projectId}/${stage}/${service}/`);
for (const [key, value] of Object.entries(params)) {
const envKey = key.split('/').pop();
console.log(`${envKey}=${value}`);
}
}
継承よりコンポジションを選ぶ理由:
- 各モジュールを個別にテストできます。モックする基底クラスは不要です
- 親クラスによる隠れた動作がありません
- 実装を簡単に入れ替えられます(例:異なるクラウドプロバイダー)
- コマンドが必要なものを明示的に宣言します。基底クラスから暗黙的に
this.log()を呼び出すことはありません
使用する状況:
- CLI ツールが 5 個以上ある大規模プロジェクト
- 複数のコマンドでコアモジュール(SSM、S3、ロギング)を共有する
- 一貫したパターンが必要なチーム環境
選択ガイド
| 要素 | 1 層 | 2 層 | 3 層 |
|---|---|---|---|
| プロジェクト規模 | シンプル | 中規模 | 大規模 |
| CLI コマンド | 5 個以下 | 2~5 個 | 5 個以上 |
| チーム規模 | 1 人 | 1~3 人 | 3 人以上 |
| 複雑さ | 最小限 | 中程度 | 複雑 |
| 開発速度 | 最速 | 速い | 初期は遅い |
| 再利用性 | なし | 低い | 高い |
| 共有ロジック | 最小限 | 一部 | 大幅 |
静的フロントエンドや単一スタックのプロジェクトでは、1 層から始めてください。 コマンドが約 200 行を超える場合や、大幅なロジックを共有する場合は、2 層に移行してください。 次の場合は、3 層に移行してください:
- CLI ツールが 5 個以上ある
- コマンド間でロジックが重複している
- チームで一貫したパターンが必要
- 抽象化によって明確な価値が得られる
言語固有の統合
JavaScript/TypeScript - package.json:
{
"scripts": {
"deploy:dev": "ts-node cli/bin/deploy.ts --env=dev",
"params:get": "ts-node cli/bin/ssm-params.ts get"
}
}
Python - pyproject.toml:
[tool.poetry.scripts]
deploy-dev = "cli.bin.deploy:main"
params-get = "cli.bin.ssm_params:get_parameter"
Makefile - 汎用:
deploy-dev:
python cli/bin/deploy.py --env=dev
params-get:
python cli/bin/ssm_params.py get $(NAME)
関連パターン
段階的な改善
開発者がこのスキルで防ぐべきだった動作を修正した場合は、今後同じ修正が必要にならないよう、このスキルに加える具体的な変更を提案してください。