From 056b1622fb8dbd7c44b2dd5bbecf404f40430f53 Mon Sep 17 00:00:00 2001 From: jacoyutorius Date: Tue, 9 Dec 2025 15:19:31 +0900 Subject: [PATCH 1/7] =?UTF-8?q?=F0=9F=93=9D=20=E3=82=A4=E3=83=B3=E3=82=BF?= =?UTF-8?q?=E3=83=BC=E3=83=95=E3=82=A7=E3=83=BC=E3=82=B9=E7=B0=A1=E7=B4=A0?= =?UTF-8?q?=E5=8C=96=E3=81=AE=E8=A6=81=E4=BB=B6=E5=AE=9A=E7=BE=A9=E6=9B=B8?= =?UTF-8?q?=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - config.yml を廃止 - Secret ID を直接引数として受け取るシンプルな設計 - configure/add/list コマンドを廃止 - デフォルト値: profile=default, region=ap-northeast-1 --- docs/simplify-interface-requirements.md | 223 ++++++++++++++++++++++++ 1 file changed, 223 insertions(+) create mode 100644 docs/simplify-interface-requirements.md diff --git a/docs/simplify-interface-requirements.md b/docs/simplify-interface-requirements.md new file mode 100644 index 0000000..d28f49b --- /dev/null +++ b/docs/simplify-interface-requirements.md @@ -0,0 +1,223 @@ +# Kagi インターフェース簡素化 要件定義 + +## 概要 + +現在の Kagi は `config.yml` でプロジェクト/環境を管理する設計だが、よりシンプルで直感的なインターフェースに変更する。 + +## 現在の問題点 + +- `config.yml` の手動編集または `kagi add` コマンドでの事前設定が必要 +- プロジェクト/環境という抽象化が必要以上に複雑 +- Secret ID を直接指定できない(`--secret-id` オプションはあるが、メインの使い方ではない) + +## 新しい設計 + +### 基本コンセプト + +**Secret ID を直接引数として受け取る**シンプルな設計に変更する。 + +### コマンドインターフェース + +#### Before (現在) +```bash +# 事前設定が必要 +kagi add compal dev --secret-id compal/dev +kagi import compal dev + +# または --secret-id オプション +kagi import --secret-id compal/dev +``` + +#### After (新設計) +```bash +# Secret ID を直接引数として指定 +kagi import compal/dev + +# AWS Profile を指定 +kagi import compal/dev --profile compal_user + +# デフォルト profile を使用 +kagi import compal/dev # --profile 未指定 = default +``` + +--- + +## 詳細仕様 + +### 1. config.yml の廃止 + +- `~/.config/kagi/config.yml` は使用しない +- プロジェクト/環境の概念を廃止 +- 全ての設定をコマンドラインオプションで指定 + +### 2. コマンド引数 + +#### `kagi import ` + +**引数:** +- `` - AWS Secrets Manager の Secret ID(必須) + - 例: `compal/dev`, `kagi/myapp/stg`, `crs/prd/app` + +**オプション:** +- `--profile PROFILE` - AWS Profile を指定(任意) + - 未指定の場合は `default` を使用 +- `--region REGION` - AWS Region を指定(任意) + - 未指定の場合は `ap-northeast-1` を使用 + +**使用例:** +```bash +# 最小限の使用 +kagi import compal/dev + +# AWS Profile を指定 +kagi import compal/dev --profile compal_user + +# Region も指定 +kagi import compal/dev --profile compal_user --region us-east-1 +``` + +#### `kagi download ` + +`import` と同様のインターフェース。 + +**追加オプション:** +- `--path PATH` - 出力先ファイルパス +- `--force` - 既存ファイルを上書き + +**使用例:** +```bash +# 標準出力 +kagi download compal/dev + +# ファイルに保存 +kagi download compal/dev --path .env + +# AWS Profile を指定してファイルに保存 +kagi download compal/dev --profile compal_user --path .env +``` + +### 3. 廃止するコマンド + +以下のコマンドは不要になるため廃止: +- `kagi configure` - デフォルト設定が不要に +- `kagi add` - 事前設定が不要に +- `kagi list` - プロジェクト一覧の概念が不要に + +### 4. 残すコマンド + +- `kagi import ` - メインコマンド +- `kagi download ` - メインコマンド +- `kagi version` - バージョン表示 + +--- + +## 実装方針 + +### 変更が必要なファイル + +1. **lib/kagi/cli.rb** + - `import` コマンドの引数を `` に変更 + - `download` コマンドの引数を `` に変更 + - `configure`, `add`, `list` コマンドを削除 + +2. **lib/kagi/config.rb** + - ファイル全体を削除(不要) + +3. **spec/kagi/config_spec.rb** + - ファイル全体を削除(不要) + +4. **spec/kagi/cli_spec.rb** + - 新しいインターフェースのテストを追加(必要に応じて) + +5. **README.md** + - 使用方法を全面的に書き換え + +6. **kagi_spec_ja.md** + - 仕様書を更新 + +### デフォルト値 + +- **AWS Profile**: `default` +- **AWS Region**: `ap-northeast-1` + +### エラーハンドリング + +- Secret ID が指定されていない場合: エラーメッセージを表示 +- AWS Profile が存在しない場合: AWS SDK のエラーをそのまま表示 +- Secret が見つからない場合: 既存のエラーメッセージを表示 + +--- + +## メリット + +1. **シンプル** - config.yml の管理が不要 +2. **直感的** - Secret ID を直接指定できる +3. **柔軟** - コマンドごとに異なる Profile/Region を指定可能 +4. **学習コスト低** - 覚えるコマンドが少ない + +## デメリットと対策 + +### デメリット1: Secret ID を毎回入力する必要がある + +**対策:** +- シェルのエイリアスや関数で対応 + ```bash + alias kagi-compal-dev='kagi import compal/dev --profile compal_user' + ``` +- 頻繁に使う Secret ID はドキュメントに記載 + +### デメリット2: プロジェクト一覧が見れない + +**対策:** +- AWS CLI で確認可能 + ```bash + aws secretsmanager list-secrets --profile compal_user + ``` +- 必要であれば README に記載 + +--- + +## 移行ガイド + +### 既存ユーザー向け + +**Before:** +```bash +kagi add compal dev --secret-id compal/dev --profile compal_user +kagi import compal dev +``` + +**After:** +```bash +kagi import compal/dev --profile compal_user +``` + +### config.yml の移行 + +既存の `config.yml` は使用されなくなるため、各プロジェクトの Secret ID を確認しておく。 + +```yaml +# 旧 config.yml +projects: + compal: + dev: + secret_id: compal/dev + profile: compal_user +``` + +↓ + +```bash +# 新しいコマンド +kagi import compal/dev --profile compal_user +``` + +--- + +## 実装スケジュール + +1. **Phase 1**: CLI インターフェースの変更 +2. **Phase 2**: Config モジュールの削除 +3. **Phase 3**: テストの更新 +4. **Phase 4**: ドキュメントの更新 +5. **Phase 5**: 動作確認とリリース From 74ac333adfc8e2eb5f27854ea8355e1d9bfd6bde Mon Sep 17 00:00:00 2001 From: jacoyutorius Date: Tue, 9 Dec 2025 16:22:04 +0900 Subject: [PATCH 2/7] =?UTF-8?q?=F0=9F=9A=80=20v0.2.0:=20=E3=82=A4=E3=83=B3?= =?UTF-8?q?=E3=82=BF=E3=83=BC=E3=83=95=E3=82=A7=E3=83=BC=E3=82=B9=E3=82=92?= =?UTF-8?q?=E5=A4=A7=E5=B9=85=E3=81=AB=E7=B0=A1=E7=B4=A0=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BREAKING CHANGES: - Secret ID を直接引数として指定する方式に変更 - config.yml を廃止(事前設定が不要に) - configure/add/list コマンドを削除 Changes: - kagi import の新しいインターフェース - kagi download の新しいインターフェース - デフォルト値: --profile=default, --region=ap-northeast-1 - Config モジュールを削除 - README.md を全面的に書き換え - CHANGELOG.md を追加 - v0.1.x からの移行ガイドを追加 --- CHANGELOG.md | 55 +++++++ README.md | 304 +++++++++++++++------------------------ lib/kagi.rb | 2 - lib/kagi/cli.rb | 183 +++-------------------- lib/kagi/config.rb | 90 ------------ lib/kagi/version.rb | 2 +- spec/kagi/config_spec.rb | 230 ----------------------------- 7 files changed, 193 insertions(+), 673 deletions(-) create mode 100644 CHANGELOG.md delete mode 100644 lib/kagi/config.rb delete mode 100644 spec/kagi/config_spec.rb diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..d7a83a0 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,55 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [0.2.0] - 2025-12-09 + +### Changed +- **BREAKING**: インターフェースを大幅に簡素化 + - Secret ID を直接引数として指定する方式に変更 + - `kagi import ` / `kagi download ` +- **BREAKING**: config.yml を廃止 + - 事前設定が不要に + - 全ての設定をコマンドラインオプションで指定 +- デフォルト値を設定 + - `--profile` のデフォルト: `default` + - `--region` のデフォルト: `ap-northeast-1` + +### Removed +- **BREAKING**: `kagi configure` コマンドを削除 +- **BREAKING**: `kagi add` コマンドを削除 +- **BREAKING**: `kagi list` コマンドを削除 +- `lib/kagi/config.rb` モジュールを削除 +- `ConfigError` クラスを削除 + +### Added +- v0.1.x からの移行ガイドを README に追加 +- シェルエイリアスの活用例を README に追加 + +### Fixed +- JSON パースエラー時のエラーメッセージを改善 + - シークレットの実際の内容(先頭100文字)を表示 + - JSON 形式が必要であることを明示 + +## [0.1.0] - 2025-12-04 + +### Added +- 初回リリース +- AWS Secrets Manager からシークレットを取得 +- dotenv 形式での `.env` ファイル生成 +- export 形式での環境変数出力 +- config.yml による設定管理 +- `kagi configure` - 初期設定 +- `kagi add` - プロジェクト/環境の追加 +- `kagi download` - .env ファイル生成 +- `kagi import` - export 文の出力 +- `kagi list` - プロジェクト一覧表示 +- `kagi version` - バージョン表示 +- RSpec によるテスト +- GitHub Actions CI/CD + +[0.2.0]: https://github.com/jacoyutorius/kagi/compare/v0.1.0...v0.2.0 +[0.1.0]: https://github.com/jacoyutorius/kagi/releases/tag/v0.1.0 diff --git a/README.md b/README.md index 00dcd37..14b2f70 100644 --- a/README.md +++ b/README.md @@ -7,10 +7,10 @@ AWS Secrets Manager から秘匿情報を取得して、ローカル開発環境 ## 特徴 - 🔐 **AWS Secrets Manager と統合** - 秘匿情報の一元管理 -- 🚀 **シンプルなコマンド** - `kagi download` で `.env` を即座に生成 -- 🎯 **柔軟な設定** - プロジェクト/環境ごとに AWS Profile/Region を管理 +- 🚀 **シンプルなコマンド** - Secret ID を直接指定するだけ - 💎 **Ruby 製** - AWS SDK を直接使用 (AWS CLI 不要) - 🔒 **安全** - `.env` ファイルを Git 管理せず、AWS IAM で権限制御 +- ⚡ **設定不要** - config.yml などの事前設定が不要 ## インストール @@ -26,163 +26,110 @@ gem 'kagi' ## 使い方 -### 1. 初期設定 +### 基本的な使用方法 -```bash -kagi configure -``` - -対話式で AWS Profile と Region を設定します。設定は `~/.config/kagi/config.yml` に保存されます。 - -### 2. プロジェクト設定 - -`~/.config/kagi/config.yml` を編集して、プロジェクトと環境を追加します: - -```yaml -defaults: - profile: my-aws-profile - region: ap-northeast-1 - -projects: - myapp: - dev: - secret_id: kagi/myapp/dev - stg: - secret_id: kagi/myapp/stg - prd: - secret_id: kagi/myapp/prd - profile: production-profile # 特定環境のみ profile を上書き可能 -``` - -**または、`kagi add` コマンドで追加:** +Secret ID を直接指定するだけで使えます: ```bash -# 対話式で追加 -kagi add myapp dev +# 環境変数を export 形式で出力 +kagi import compal/dev -# オプション指定で追加 -kagi add myapp stg --secret-id kagi/myapp/stg -kagi add myapp prd --secret-id kagi/myapp/prd --profile production-profile +# .env ファイルを生成 +kagi download compal/dev --path .env ``` -### 3. .env ファイルを生成 +### AWS Profile を指定 -```bash -# 標準出力に表示 -kagi download myapp dev - -# ファイルに保存 -kagi download myapp dev --path .env.development.local - -# 既存ファイルを上書き -kagi download myapp dev --path .env --force -``` - -**設定なしで直接 Secret ID を指定:** +複数の AWS アカウントを使い分ける場合: ```bash -# --secret-id オプションで直接実行 -kagi download --secret-id kagi/myapp/dev -kagi download --secret-id kagi/myapp/dev --path .env -``` +# AWS Profile を指定 +kagi import compal/dev --profile compal_user -### 4. 環境変数をエクスポート - -```bash -# export 文を出力 -kagi import myapp dev - -# 現在のシェルに読み込む -eval "$(kagi import myapp dev)" +# Region も指定 +kagi import compal/dev --profile compal_user --region us-east-1 ``` -**設定なしで直接 Secret ID を指定:** +### ファイルへの出力 ```bash -kagi import --secret-id kagi/myapp/dev +# .env ファイルに保存 +kagi download compal/dev --path .env + +# 既存ファイルを上書き +kagi download compal/dev --path .env --force ``` -### 5. プロジェクト一覧を表示 +### シェルに環境変数を読み込む ```bash -kagi list +# 現在のシェルに環境変数を読み込む +eval "$(kagi import compal/dev)" ``` ## コマンドリファレンス -### `kagi configure` - -初期設定を行います。デフォルトの AWS Profile と Region を対話式で設定します。 +### `kagi import ` -### `kagi add ` +環境変数を export する形式で出力します。 -プロジェクト/環境を設定に追加します。対話式またはオプション指定で追加できます。 +**引数:** +- `` - AWS Secrets Manager の Secret ID(必須) **オプション:** -- `--secret-id SECRET_ID` - Secret ID を指定 -- `--profile PROFILE` - AWS Profile を指定 -- `--region REGION` - AWS Region を指定 +- `--profile PROFILE` - AWS Profile を指定(デフォルト: `default`) +- `--region REGION` - AWS Region を指定(デフォルト: `ap-northeast-1`) **使用例:** ```bash -# 対話式 -kagi add myapp dev +# 最小限の使用 +kagi import compal/dev + +# AWS Profile を指定 +kagi import compal/dev --profile compal_user -# オプション指定 -kagi add myapp prd --secret-id kagi/myapp/prd --profile prod-profile +# シェルに読み込む +eval "$(kagi import compal/dev)" ``` -### `kagi download [project] [env]` +### `kagi download ` AWS Secrets Manager からシークレットを取得し、dotenv 形式で出力します。 +**引数:** +- `` - AWS Secrets Manager の Secret ID(必須) + **オプション:** -- `--secret-id SECRET_ID` - Secret ID を直接指定(この場合 project/env は不要) +- `--profile PROFILE` - AWS Profile を指定(デフォルト: `default`) +- `--region REGION` - AWS Region を指定(デフォルト: `ap-northeast-1`) - `--path PATH` - 出力先ファイルパス - `--force` - 既存ファイルを上書き -- `--profile PROFILE` - AWS Profile を指定 (設定を上書き) -- `--region REGION` - AWS Region を指定 (設定を上書き) **使用例:** ```bash -# 設定から取得 -kagi download myapp dev - -# Secret ID を直接指定 -kagi download --secret-id kagi/myapp/dev --path .env -``` - -### `kagi import [project] [env]` - -環境変数を export する形式で出力します。 - -**オプション:** -- `--secret-id SECRET_ID` - Secret ID を直接指定(この場合 project/env は不要) -- `--profile PROFILE` - AWS Profile を指定 -- `--region REGION` - AWS Region を指定 +# 標準出力に表示 +kagi download compal/dev -**使用例:** -```bash -# 設定から取得 -eval "$(kagi import myapp dev)" +# ファイルに保存 +kagi download compal/dev --path .env -# Secret ID を直接指定 -eval "$(kagi import --secret-id kagi/myapp/dev)" +# AWS Profile を指定してファイルに保存 +kagi download compal/dev --profile compal_user --path .env ``` -### `kagi list` - -設定されているプロジェクト/環境の一覧を表示します。 - ### `kagi version` バージョン情報を表示します。 +```bash +kagi version +``` + ## AWS Secrets Manager の設定 Secrets Manager では、1つの Secret に JSON 形式で環境変数を保存します: -**SecretId:** `kagi/myapp/dev` +**SecretId:** `compal/dev` **SecretString:** ```json @@ -193,15 +140,6 @@ Secrets Manager では、1つの Secret に JSON 形式で環境変数を保存 } ``` -## AWS Profile/Region の優先順位 - -最終的に使用される AWS Profile/Region は以下の優先順位で決定されます: - -1. CLI の `--profile` / `--region` オプション -2. `config.yml` の `projects...profile` / `region` -3. `config.yml` の `defaults.profile` / `region` -4. `"default"` / `"ap-northeast-1"` - ## 必要な IAM 権限 Kagi を使用するには、以下の IAM 権限が必要です: @@ -215,113 +153,108 @@ Kagi を使用するには、以下の IAM 権限が必要です: "Action": [ "secretsmanager:GetSecretValue" ], - "Resource": "arn:aws:secretsmanager:*:*:secret:kagi/*" + "Resource": "arn:aws:secretsmanager:*:*:secret:*" } ] } ``` -## 開発 +## シェルエイリアスの活用 -### セットアップ +頻繁に使う Secret ID はシェルのエイリアスに登録すると便利です: ```bash -# リポジトリをクローン -git clone https://github.com/yourusername/kagi.git -cd kagi - -# 依存関係のインストール -bundle install +# ~/.zshrc または ~/.bashrc +alias kagi-compal-dev='kagi import compal/dev --profile compal_user' +alias kagi-compal-stg='kagi import compal/stg --profile compal_user' ``` -### テストの実行 - +使用例: ```bash -# 全テストを実行 -bundle exec rspec +# エイリアスで簡単に実行 +eval "$(kagi-compal-dev)" +``` -# 特定のテストファイルを実行 -bundle exec rspec spec/kagi/config_spec.rb +## v0.1.x からの移行ガイド -# verbose モードで実行 -bundle exec rspec --format documentation -``` +### 主な変更点 -### 開発時の動作確認 +v0.2.0 では、よりシンプルで直感的なインターフェースに変更されました: -開発中は `bundle exec exe/kagi` でコマンドを実行できます。 +- ✅ **config.yml が不要に** - 事前設定なしで使用可能 +- ✅ **Secret ID を直接指定** - プロジェクト/環境の抽象化を廃止 +- ❌ **廃止されたコマンド**: `configure`, `add`, `list` -#### 1. ヘルプの確認 +### 移行方法 +**Before (v0.1.x):** ```bash -bundle exec exe/kagi --help -bundle exec exe/kagi download --help +# 事前設定が必要 +kagi add compal dev --secret-id compal/dev --profile compal_user +kagi import compal dev ``` -#### 2. テスト用の設定ファイルを作成 - -まず、テスト用の設定を作成します: - +**After (v0.2.0):** ```bash -# configure コマンドで初期設定 -bundle exec exe/kagi configure +# Secret ID を直接指定 +kagi import compal/dev --profile compal_user ``` -または、手動で `~/.config/kagi/config.yml` を作成: +### config.yml の確認 -```yaml -defaults: - profile: default - region: ap-northeast-1 +v0.1.x で使用していた `~/.config/kagi/config.yml` から Secret ID を確認できます: +```yaml +# 旧 config.yml projects: - testapp: + compal: dev: - secret_id: kagi/testapp/dev + secret_id: compal/dev + profile: compal_user ``` -#### 3. AWS Secrets Manager にテストデータを作成 - -AWS CLI または AWS Console で、テスト用のシークレットを作成します: - +この場合、新しいコマンドは: ```bash -aws secretsmanager create-secret \ - --name kagi/testapp/dev \ - --secret-string '{"DATABASE_URL":"postgres://localhost/testdb","API_KEY":"test-key-123"}' \ - --region ap-northeast-1 \ - --profile default +kagi import compal/dev --profile compal_user ``` -#### 4. コマンドの動作確認 +## 開発 + +### セットアップ ```bash -# プロジェクト一覧を表示 -bundle exec exe/kagi list +# リポジトリをクローン +git clone https://github.com/jacoyutorius/kagi.git +cd kagi -# 標準出力に表示 -bundle exec exe/kagi download testapp dev +# 依存関係のインストール +bundle install +``` -# ファイルに保存 -bundle exec exe/kagi download testapp dev --path .env.test +### テストの実行 -# export 形式で出力 -bundle exec exe/kagi import testapp dev +```bash +# 全テストを実行 +bundle exec rspec -# バージョン確認 -bundle exec exe/kagi version +# verbose モードで実行 +bundle exec rspec --format documentation ``` -#### 5. デバッグ +### 開発時の動作確認 -コードにデバッグポイントを追加する場合: +開発中は `bundle exec exe/kagi` でコマンドを実行できます: -```ruby -# lib/kagi/cli.rb など -require 'debug' -binding.break # ここでブレークポイント -``` +```bash +# ヘルプの確認 +bundle exec exe/kagi --help -実行時に対話的デバッガが起動します。 +# コマンドの実行 +bundle exec exe/kagi import compal/dev + +# バージョン確認 +bundle exec exe/kagi version +``` ### Gem のビルドとインストール @@ -330,24 +263,15 @@ binding.break # ここでブレークポイント gem build kagi.gemspec # ローカルにインストール -gem install kagi-0.1.0.gem +gem install kagi-0.2.0.gem # インストール後は bundle exec なしで実行可能 -kagi --help -kagi download testapp dev +kagi import compal/dev # アンインストール gem uninstall kagi ``` -### コードスタイル - -Ruby の標準的なスタイルガイドに従っています: - -- インデント: 2スペース -- 文字列: ダブルクォート推奨 -- `frozen_string_literal: true` を各ファイルの先頭に記載 - ## ライセンス MIT License - 詳細は [LICENSE](LICENSE) を参照してください。 diff --git a/lib/kagi.rb b/lib/kagi.rb index 4748724..4ab9c9d 100644 --- a/lib/kagi.rb +++ b/lib/kagi.rb @@ -1,13 +1,11 @@ # frozen_string_literal: true require_relative "kagi/version" -require_relative "kagi/config" require_relative "kagi/secrets" require_relative "kagi/env_formatter" require_relative "kagi/cli" module Kagi class Error < StandardError; end - class ConfigError < Error; end class SecretsError < Error; end end diff --git a/lib/kagi/cli.rb b/lib/kagi/cli.rb index e5df83a..730c51a 100644 --- a/lib/kagi/cli.rb +++ b/lib/kagi/cli.rb @@ -4,67 +4,35 @@ module Kagi class CLI < Thor - class_option :profile, type: :string, desc: "AWS Profile 名" - class_option :region, type: :string, desc: "AWS Region" - - desc "configure", "初期設定を行う" - def configure - puts "Kagi の初期設定を開始します" - puts - - # 既存の設定を読み込む - config = Config.load - - # デフォルト値を取得 - current_profile = config.dig("defaults", "profile") || "default" - current_region = config.dig("defaults", "region") || "ap-northeast-1" - - # ユーザー入力 - print "AWS profile (default: #{current_profile}): " - profile = $stdin.gets.chomp - profile = current_profile if profile.empty? - - print "AWS region (default: #{current_region}): " - region = $stdin.gets.chomp - region = current_region if region.empty? - - # 設定を保存 - config["defaults"] ||= {} - config["defaults"]["profile"] = profile - config["defaults"]["region"] = region - - Config.save(config) + desc "import SECRET_ID", "環境変数を export する形式で出力する" + option :profile, type: :string, desc: "AWS Profile", default: "default" + option :region, type: :string, desc: "AWS Region", default: "ap-northeast-1" + def import(secret_id) + profile = options[:profile] + region = options[:region] + + # シークレットを取得 + secrets = Secrets.fetch(secret_id, profile: profile, region: region) + + # export 形式に変換 + export_content = EnvFormatter.to_exports(secrets) - puts - puts "設定を保存しました: #{Config::CONFIG_PATH}" + puts export_content + rescue SecretsError => e + error e.message rescue => e - error "設定の保存に失敗しました: #{e.message}" + error "予期しないエラーが発生しました: #{e.message}" end - desc "download [PROJECT] [ENV]", ".env ファイルを生成する" - option :secret_id, type: :string, desc: "Secret ID (直接指定)" + desc "download SECRET_ID", ".env ファイルを生成する" + option :profile, type: :string, desc: "AWS Profile", default: "default" + option :region, type: :string, desc: "AWS Region", default: "ap-northeast-1" option :path, type: :string, desc: "出力先ファイルパス" option :force, type: :boolean, default: false, desc: "既存ファイルを上書きする" - def download(project = nil, env = nil) - config = Config.load + def download(secret_id) + profile = options[:profile] + region = options[:region] - # --secret-id が指定された場合は直接実行 - if options[:secret_id] - secret_id = options[:secret_id] - profile = options[:profile] || config.dig("defaults", "profile") || "default" - region = options[:region] || config.dig("defaults", "region") || "ap-northeast-1" - else - # project/env が指定されていない場合はエラー - unless project && env - error "PROJECT と ENV を指定するか、--secret-id オプションを使用してください" - end - - # AWS Profile/Region を解決 - profile = Config.resolve_profile(config, project, env, options[:profile]) - region = Config.resolve_region(config, project, env, options[:region]) - secret_id = Config.get_secret_id(config, project, env) - end - # シークレットを取得 secrets = Secrets.fetch(secret_id, profile: profile, region: region) @@ -77,117 +45,12 @@ def download(project = nil, env = nil) else puts env_content end - rescue ConfigError, SecretsError => e - error e.message - rescue => e - error "予期しないエラーが発生しました: #{e.message}" - end - - desc "import [PROJECT] [ENV]", "環境変数を export する形式で出力する" - option :secret_id, type: :string, desc: "Secret ID (直接指定)" - def import(project = nil, env = nil) - config = Config.load - - # --secret-id が指定された場合は直接実行 - if options[:secret_id] - secret_id = options[:secret_id] - profile = options[:profile] || config.dig("defaults", "profile") || "default" - region = options[:region] || config.dig("defaults", "region") || "ap-northeast-1" - else - # project/env が指定されていない場合はエラー - unless project && env - error "PROJECT と ENV を指定するか、--secret-id オプションを使用してください" - end - - # AWS Profile/Region を解決 - profile = Config.resolve_profile(config, project, env, options[:profile]) - region = Config.resolve_region(config, project, env, options[:region]) - secret_id = Config.get_secret_id(config, project, env) - end - - # シークレットを取得 - secrets = Secrets.fetch(secret_id, profile: profile, region: region) - - # export 形式に変換 - export_content = EnvFormatter.to_exports(secrets) - - puts export_content - rescue ConfigError, SecretsError => e + rescue SecretsError => e error e.message rescue => e error "予期しないエラーが発生しました: #{e.message}" end - desc "add PROJECT ENV", "プロジェクト/環境を設定に追加する" - option :secret_id, type: :string, desc: "Secret ID" - option :profile, type: :string, desc: "AWS Profile" - option :region, type: :string, desc: "AWS Region" - def add(project, env) - config = Config.load - - # Secret ID を取得(オプションまたは対話式) - if options[:secret_id] - secret_id = options[:secret_id] - else - print "Secret ID: " - secret_id = $stdin.gets.chomp - if secret_id.empty? - error "Secret ID は必須です" - end - end - - # Profile を取得(オプションまたは対話式) - if options[:profile] - profile = options[:profile] - else - current_profile = config.dig("defaults", "profile") || "default" - print "AWS Profile (default: #{current_profile}): " - input = $stdin.gets.chomp - profile = input.empty? ? nil : input - end - - # Region を取得(オプションまたは対話式) - if options[:region] - region = options[:region] - else - current_region = config.dig("defaults", "region") || "ap-northeast-1" - print "AWS Region (default: #{current_region}): " - input = $stdin.gets.chomp - region = input.empty? ? nil : input - end - - # 設定に追加 - config = Config.add_project(config, project, env, - secret_id: secret_id, - profile: profile, - region: region) - - # 保存 - Config.save(config) - - puts "✓ Added #{project}.#{env} to #{Config::CONFIG_PATH}" - rescue => e - error "プロジェクトの追加に失敗しました: #{e.message}" - end - - desc "list", "プロジェクト一覧を表示する" - def list - config = Config.load - projects = Config.list_projects(config) - - if projects.empty? - puts "プロジェクトが登録されていません" - puts "config.yml にプロジェクトを追加してください: #{Config::CONFIG_PATH}" - return - end - - projects.each do |proj| - puts "#{proj[:project]}.#{proj[:env]} (secret_id=#{proj[:secret_id]})" - end - rescue => e - error "プロジェクト一覧の取得に失敗しました: #{e.message}" - end - desc "version", "バージョンを表示する" def version puts "Kagi version #{Kagi::VERSION}" diff --git a/lib/kagi/config.rb b/lib/kagi/config.rb deleted file mode 100644 index f0ee9ae..0000000 --- a/lib/kagi/config.rb +++ /dev/null @@ -1,90 +0,0 @@ -# frozen_string_literal: true - -require "yaml" -require "fileutils" - -module Kagi - module Config - CONFIG_DIR = File.join(Dir.home, ".config", "kagi") - CONFIG_PATH = File.join(CONFIG_DIR, "config.yml") - - module_function - - # 設定ファイルを読み込む - def load - return {} unless File.exist?(CONFIG_PATH) - - YAML.load_file(CONFIG_PATH) || {} - rescue Psych::SyntaxError => e - raise ConfigError, "設定ファイルの読み込みに失敗しました: #{e.message}" - end - - # 設定ファイルを保存する - def save(config) - FileUtils.mkdir_p(CONFIG_DIR) - File.write(CONFIG_PATH, YAML.dump(config)) - end - - # AWS Profile を優先順位に従って解決する - # 優先順位: CLI オプション > project/env.profile > defaults.profile > "default" - def resolve_profile(config, project, env, cli_profile = nil) - return cli_profile if cli_profile - - project_config = config.dig("projects", project, env) - return project_config["profile"] if project_config&.key?("profile") - - config.dig("defaults", "profile") || "default" - end - - # AWS Region を優先順位に従って解決する - def resolve_region(config, project, env, cli_region = nil) - return cli_region if cli_region - - project_config = config.dig("projects", project, env) - return project_config["region"] if project_config&.key?("region") - - config.dig("defaults", "region") || "ap-northeast-1" - end - - # プロジェクト/環境の SecretId を取得する - def get_secret_id(config, project, env) - secret_id = config.dig("projects", project, env, "secret_id") - raise ConfigError, "SecretId が見つかりません: #{project}.#{env}" unless secret_id - - secret_id - end - - # プロジェクト一覧を取得する - def list_projects(config) - projects = config["projects"] || {} - result = [] - - projects.each do |project_name, envs| - envs.each do |env_name, env_config| - result << { - project: project_name, - env: env_name, - secret_id: env_config["secret_id"], - profile: env_config["profile"], - region: env_config["region"] - } - end - end - - result - end - - # プロジェクト/環境を追加する - def add_project(config, project, env, secret_id:, profile: nil, region: nil) - config["projects"] ||= {} - config["projects"][project] ||= {} - config["projects"][project][env] = { - "secret_id" => secret_id - } - config["projects"][project][env]["profile"] = profile if profile - config["projects"][project][env]["region"] = region if region - - config - end - end -end diff --git a/lib/kagi/version.rb b/lib/kagi/version.rb index ee77a2e..e03a38a 100644 --- a/lib/kagi/version.rb +++ b/lib/kagi/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module Kagi - VERSION = "0.1.0" + VERSION = "0.2.0" end diff --git a/spec/kagi/config_spec.rb b/spec/kagi/config_spec.rb deleted file mode 100644 index a88bbf3..0000000 --- a/spec/kagi/config_spec.rb +++ /dev/null @@ -1,230 +0,0 @@ -# frozen_string_literal: true - -require "spec_helper" -require "kagi" -require "tmpdir" -require "fileutils" - -RSpec.describe Kagi::Config do - let(:test_dir) { Dir.mktmpdir } - let(:config_path) { File.join(test_dir, "config.yml") } - - before do - stub_const("Kagi::Config::CONFIG_DIR", test_dir) - stub_const("Kagi::Config::CONFIG_PATH", config_path) - end - - after do - FileUtils.rm_rf(test_dir) - end - - describe ".load" do - context "設定ファイルが存在しない場合" do - it "空のハッシュを返す" do - expect(described_class.load).to eq({}) - end - end - - context "設定ファイルが存在する場合" do - it "設定を読み込む" do - config = { - "defaults" => { - "profile" => "test-profile", - "region" => "us-west-2" - } - } - described_class.save(config) - - expect(described_class.load).to eq(config) - end - end - end - - describe ".save" do - it "設定ファイルを保存する" do - config = { - "defaults" => { - "profile" => "my-profile", - "region" => "ap-northeast-1" - } - } - - described_class.save(config) - - expect(File.exist?(config_path)).to be true - loaded = described_class.load - expect(loaded).to eq(config) - end - end - - describe ".resolve_profile" do - let(:config) do - { - "defaults" => { "profile" => "default-profile" }, - "projects" => { - "myapp" => { - "dev" => { - "secret_id" => "kagi/myapp/dev", - "profile" => "dev-profile" - }, - "prd" => { - "secret_id" => "kagi/myapp/prd" - } - } - } - } - end - - it "CLI オプションが最優先" do - result = described_class.resolve_profile(config, "myapp", "dev", "cli-profile") - expect(result).to eq("cli-profile") - end - - it "プロジェクト設定が次に優先" do - result = described_class.resolve_profile(config, "myapp", "dev") - expect(result).to eq("dev-profile") - end - - it "デフォルト設定が次に優先" do - result = described_class.resolve_profile(config, "myapp", "prd") - expect(result).to eq("default-profile") - end - - it "何も設定されていない場合は 'default' を返す" do - result = described_class.resolve_profile({}, "myapp", "dev") - expect(result).to eq("default") - end - end - - describe ".resolve_region" do - let(:config) do - { - "defaults" => { "region" => "us-east-1" }, - "projects" => { - "myapp" => { - "dev" => { - "secret_id" => "kagi/myapp/dev", - "region" => "ap-northeast-1" - } - } - } - } - end - - it "CLI オプションが最優先" do - result = described_class.resolve_region(config, "myapp", "dev", "us-west-2") - expect(result).to eq("us-west-2") - end - - it "プロジェクト設定が次に優先" do - result = described_class.resolve_region(config, "myapp", "dev") - expect(result).to eq("ap-northeast-1") - end - - it "デフォルト設定が次に優先" do - result = described_class.resolve_region(config, "myapp", "prd") - expect(result).to eq("us-east-1") - end - - it "何も設定されていない場合は 'ap-northeast-1' を返す" do - result = described_class.resolve_region({}, "myapp", "dev") - expect(result).to eq("ap-northeast-1") - end - end - - describe ".get_secret_id" do - let(:config) do - { - "projects" => { - "myapp" => { - "dev" => { "secret_id" => "kagi/myapp/dev" } - } - } - } - end - - it "SecretId を返す" do - result = described_class.get_secret_id(config, "myapp", "dev") - expect(result).to eq("kagi/myapp/dev") - end - - it "SecretId が見つからない場合はエラーを発生させる" do - expect { - described_class.get_secret_id(config, "myapp", "prd") - }.to raise_error(Kagi::ConfigError, /SecretId が見つかりません/) - end - end - - describe ".list_projects" do - let(:config) do - { - "projects" => { - "app1" => { - "dev" => { "secret_id" => "kagi/app1/dev" }, - "prd" => { "secret_id" => "kagi/app1/prd", "profile" => "prd-profile" } - }, - "app2" => { - "stg" => { "secret_id" => "kagi/app2/stg" } - } - } - } - end - - it "プロジェクト一覧を返す" do - result = described_class.list_projects(config) - - expect(result).to contain_exactly( - { project: "app1", env: "dev", secret_id: "kagi/app1/dev", profile: nil, region: nil }, - { project: "app1", env: "prd", secret_id: "kagi/app1/prd", profile: "prd-profile", region: nil }, - { project: "app2", env: "stg", secret_id: "kagi/app2/stg", profile: nil, region: nil } - ) - end - - it "プロジェクトが空の場合は空配列を返す" do - result = described_class.list_projects({}) - expect(result).to eq([]) - end - end - - describe ".add_project" do - it "プロジェクト/環境を追加する" do - config = {} - - result = described_class.add_project(config, "myapp", "dev", secret_id: "kagi/myapp/dev") - - expect(result["projects"]["myapp"]["dev"]["secret_id"]).to eq("kagi/myapp/dev") - expect(result["projects"]["myapp"]["dev"]["profile"]).to be_nil - expect(result["projects"]["myapp"]["dev"]["region"]).to be_nil - end - - it "profile と region を指定して追加する" do - config = {} - - result = described_class.add_project( - config, "myapp", "prd", - secret_id: "kagi/myapp/prd", - profile: "prod-profile", - region: "us-east-1" - ) - - expect(result["projects"]["myapp"]["prd"]["secret_id"]).to eq("kagi/myapp/prd") - expect(result["projects"]["myapp"]["prd"]["profile"]).to eq("prod-profile") - expect(result["projects"]["myapp"]["prd"]["region"]).to eq("us-east-1") - end - - it "既存のプロジェクトに新しい環境を追加する" do - config = { - "projects" => { - "myapp" => { - "dev" => { "secret_id" => "kagi/myapp/dev" } - } - } - } - - result = described_class.add_project(config, "myapp", "stg", secret_id: "kagi/myapp/stg") - - expect(result["projects"]["myapp"]["dev"]["secret_id"]).to eq("kagi/myapp/dev") - expect(result["projects"]["myapp"]["stg"]["secret_id"]).to eq("kagi/myapp/stg") - end - end -end From 31c7548a27f5c7f13da71d59098c267cb65f462b Mon Sep 17 00:00:00 2001 From: jacoyutorius Date: Tue, 9 Dec 2025 16:29:32 +0900 Subject: [PATCH 3/7] =?UTF-8?q?=E2=9C=A8=20=E7=92=B0=E5=A2=83=E5=A4=89?= =?UTF-8?q?=E6=95=B0=E3=81=AE=E8=AA=8D=E8=A8=BC=E6=83=85=E5=A0=B1=E3=82=92?= =?UTF-8?q?=E5=84=AA=E5=85=88=E7=9A=84=E3=81=AB=E4=BD=BF=E7=94=A8=E3=81=99?= =?UTF-8?q?=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB=E6=94=B9=E5=96=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AWS_ACCESS_KEY_ID と AWS_SECRET_ACCESS_KEY が設定されている場合は環境変数を優先 - AWS_SESSION_TOKEN にも対応 - 一時的な認証情報(Session Token付き)が使えるようになった --- lib/kagi/secrets.rb | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/lib/kagi/secrets.rb b/lib/kagi/secrets.rb index f41072d..983ce4a 100644 --- a/lib/kagi/secrets.rb +++ b/lib/kagi/secrets.rb @@ -29,11 +29,15 @@ def fetch(secret_id, profile:, region:) # AWS クライアントを作成する def create_client(profile:, region:) - credentials = if profile == "default" - # default の場合は明示的に credentials を指定しない - # (環境変数や IAM Role を優先) + # 環境変数が設定されている場合は優先的に使用 + credentials = if ENV['AWS_ACCESS_KEY_ID'] && ENV['AWS_SECRET_ACCESS_KEY'] + # 環境変数から認証情報を取得(Session Token にも対応) + nil # credentials を nil にすると AWS SDK が環境変数を自動的に使用 + elsif profile == "default" + # default の場合も環境変数や IAM Role を優先 nil else + # 指定されたプロファイルを使用 Aws::SharedCredentials.new(profile_name: profile) end From 6ab74f520134c307e26c93f62d52d65beaffe06d Mon Sep 17 00:00:00 2001 From: jacoyutorius Date: Tue, 9 Dec 2025 17:07:11 +0900 Subject: [PATCH 4/7] =?UTF-8?q?=E2=9C=A8=20--debug=20=E3=82=AA=E3=83=97?= =?UTF-8?q?=E3=82=B7=E3=83=A7=E3=83=B3=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - import/download コマンドに --debug オプションを追加 - デバッグログの出力を制御可能に - デフォルトではデバッグログは表示されない --- lib/kagi/cli.rb | 8 ++++++-- lib/kagi/secrets.rb | 33 ++++++++++++++++++++++++--------- 2 files changed, 30 insertions(+), 11 deletions(-) diff --git a/lib/kagi/cli.rb b/lib/kagi/cli.rb index 730c51a..87d1df0 100644 --- a/lib/kagi/cli.rb +++ b/lib/kagi/cli.rb @@ -7,12 +7,14 @@ class CLI < Thor desc "import SECRET_ID", "環境変数を export する形式で出力する" option :profile, type: :string, desc: "AWS Profile", default: "default" option :region, type: :string, desc: "AWS Region", default: "ap-northeast-1" + option :debug, type: :boolean, desc: "デバッグログを表示", default: false def import(secret_id) profile = options[:profile] region = options[:region] + debug = options[:debug] # シークレットを取得 - secrets = Secrets.fetch(secret_id, profile: profile, region: region) + secrets = Secrets.fetch(secret_id, profile: profile, region: region, debug: debug) # export 形式に変換 export_content = EnvFormatter.to_exports(secrets) @@ -29,12 +31,14 @@ def import(secret_id) option :region, type: :string, desc: "AWS Region", default: "ap-northeast-1" option :path, type: :string, desc: "出力先ファイルパス" option :force, type: :boolean, default: false, desc: "既存ファイルを上書きする" + option :debug, type: :boolean, desc: "デバッグログを表示", default: false def download(secret_id) profile = options[:profile] region = options[:region] + debug = options[:debug] # シークレットを取得 - secrets = Secrets.fetch(secret_id, profile: profile, region: region) + secrets = Secrets.fetch(secret_id, profile: profile, region: region, debug: debug) # dotenv 形式に変換 env_content = EnvFormatter.to_env(secrets) diff --git a/lib/kagi/secrets.rb b/lib/kagi/secrets.rb index 983ce4a..dad6abc 100644 --- a/lib/kagi/secrets.rb +++ b/lib/kagi/secrets.rb @@ -8,8 +8,8 @@ module Secrets module_function # AWS Secrets Manager からシークレットを取得する - def fetch(secret_id, profile:, region:) - client = create_client(profile: profile, region: region) + def fetch(secret_id, profile:, region:, debug: false) + client = create_client(profile: profile, region: region, debug: debug) resp = client.get_secret_value(secret_id: secret_id) JSON.parse(resp.secret_string) @@ -23,21 +23,36 @@ def fetch(secret_id, profile:, region:) raise SecretsError, "AWS 認証情報が見つかりません。AWS Profile '#{profile}' を確認してください。" rescue Aws::SecretsManager::Errors::ServiceError => e raise SecretsError, "AWS Secrets Manager エラー: #{e.message}" - rescue JSON::ParserError - raise SecretsError, "シークレットの JSON パースに失敗しました" + rescue JSON::ParserError => e + # デバッグ用に実際のシークレット内容を表示(先頭100文字のみ) + preview = resp.secret_string[0..100] + raise SecretsError, "シークレットの JSON パースに失敗しました。\n" \ + "シークレットは JSON 形式である必要があります。\n" \ + "実際の内容(先頭100文字): #{preview.inspect}\n" \ + "パースエラー: #{e.message}" end # AWS クライアントを作成する - def create_client(profile:, region:) + def create_client(profile:, region:, debug: false) + # デバッグ: 環境変数の状態を確認 + has_env_creds = ENV['AWS_ACCESS_KEY_ID'] && ENV['AWS_SECRET_ACCESS_KEY'] + # 環境変数が設定されている場合は優先的に使用 - credentials = if ENV['AWS_ACCESS_KEY_ID'] && ENV['AWS_SECRET_ACCESS_KEY'] - # 環境変数から認証情報を取得(Session Token にも対応) - nil # credentials を nil にすると AWS SDK が環境変数を自動的に使用 + credentials = if has_env_creds + # 環境変数から明示的に認証情報を作成(Session Token にも対応) + $stderr.puts "DEBUG: 環境変数から認証情報を使用します" if debug + Aws::Credentials.new( + ENV['AWS_ACCESS_KEY_ID'], + ENV['AWS_SECRET_ACCESS_KEY'], + ENV['AWS_SESSION_TOKEN'] # Session Token がない場合は nil になる + ) elsif profile == "default" - # default の場合も環境変数や IAM Role を優先 + # default の場合は credentials を指定しない(IAM Role などを優先) + $stderr.puts "DEBUG: デフォルトの認証情報を使用します" if debug nil else # 指定されたプロファイルを使用 + $stderr.puts "DEBUG: AWS Profile '#{profile}' を使用します" if debug Aws::SharedCredentials.new(profile_name: profile) end From 8e8f07d14c68478e3321c430b52490a8af629d27 Mon Sep 17 00:00:00 2001 From: jacoyutorius Date: Tue, 9 Dec 2025 17:15:44 +0900 Subject: [PATCH 5/7] =?UTF-8?q?=E2=9C=A8=20aws=20login=20=E3=81=AE?= =?UTF-8?q?=E8=AA=8D=E8=A8=BC=E6=83=85=E5=A0=B1=E3=81=AB=E5=AF=BE=E5=BF=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AWS_PROFILE 環境変数を使用して AWS SDK のデフォルト認証情報チェーンに任せる - Aws::SharedCredentials を使わず credentials: nil を渡す - aws login, aws sso login, ~/.aws/credentials の全てに対応 --- lib/kagi/secrets.rb | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/lib/kagi/secrets.rb b/lib/kagi/secrets.rb index dad6abc..032e489 100644 --- a/lib/kagi/secrets.rb +++ b/lib/kagi/secrets.rb @@ -37,23 +37,21 @@ def create_client(profile:, region:, debug: false) # デバッグ: 環境変数の状態を確認 has_env_creds = ENV['AWS_ACCESS_KEY_ID'] && ENV['AWS_SECRET_ACCESS_KEY'] - # 環境変数が設定されている場合は優先的に使用 - credentials = if has_env_creds - # 環境変数から明示的に認証情報を作成(Session Token にも対応) + # 認証方法を判定 + if has_env_creds $stderr.puts "DEBUG: 環境変数から認証情報を使用します" if debug - Aws::Credentials.new( + # 環境変数から明示的に認証情報を作成(Session Token にも対応) + credentials = Aws::Credentials.new( ENV['AWS_ACCESS_KEY_ID'], ENV['AWS_SECRET_ACCESS_KEY'], ENV['AWS_SESSION_TOKEN'] # Session Token がない場合は nil になる ) - elsif profile == "default" - # default の場合は credentials を指定しない(IAM Role などを優先) - $stderr.puts "DEBUG: デフォルトの認証情報を使用します" if debug - nil else - # 指定されたプロファイルを使用 - $stderr.puts "DEBUG: AWS Profile '#{profile}' を使用します" if debug - Aws::SharedCredentials.new(profile_name: profile) + # プロファイルを環境変数で指定し、AWS SDK のデフォルト認証情報チェーンに任せる + # これで aws login, aws sso login, ~/.aws/credentials の全てに対応 + $stderr.puts "DEBUG: AWS Profile '#{profile}' を使用します (AWS SDK のデフォルト認証情報チェーン)" if debug + ENV['AWS_PROFILE'] = profile unless profile == 'default' + credentials = nil end Aws::SecretsManager::Client.new( From 1ffd2337d592d66b06582e22ced6e4c8db91f12f Mon Sep 17 00:00:00 2001 From: jacoyutorius Date: Tue, 9 Dec 2025 17:18:12 +0900 Subject: [PATCH 6/7] =?UTF-8?q?=F0=9F=90=9B=20AWS=20SDK=20=E3=81=AB=20prof?= =?UTF-8?q?ile=20=E3=82=AA=E3=83=97=E3=82=B7=E3=83=A7=E3=83=B3=E3=82=92?= =?UTF-8?q?=E7=9B=B4=E6=8E=A5=E6=B8=A1=E3=81=99=E3=82=88=E3=81=86=E3=81=AB?= =?UTF-8?q?=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ENV['AWS_PROFILE'] ではなく Client.new(profile: profile) を使用 - これで aws login の認証情報が正しく読み込まれる --- lib/kagi/secrets.rb | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/lib/kagi/secrets.rb b/lib/kagi/secrets.rb index 032e489..36b30e3 100644 --- a/lib/kagi/secrets.rb +++ b/lib/kagi/secrets.rb @@ -46,18 +46,23 @@ def create_client(profile:, region:, debug: false) ENV['AWS_SECRET_ACCESS_KEY'], ENV['AWS_SESSION_TOKEN'] # Session Token がない場合は nil になる ) + + Aws::SecretsManager::Client.new( + region: region, + credentials: credentials + ) else - # プロファイルを環境変数で指定し、AWS SDK のデフォルト認証情報チェーンに任せる + # プロファイルを指定して AWS SDK のデフォルト認証情報チェーンに任せる # これで aws login, aws sso login, ~/.aws/credentials の全てに対応 $stderr.puts "DEBUG: AWS Profile '#{profile}' を使用します (AWS SDK のデフォルト認証情報チェーン)" if debug - ENV['AWS_PROFILE'] = profile unless profile == 'default' - credentials = nil + + # profile オプションを渡して Client を作成 + # これで AWS SDK が自動的に適切な認証情報を見つける + Aws::SecretsManager::Client.new( + region: region, + profile: profile + ) end - - Aws::SecretsManager::Client.new( - region: region, - credentials: credentials - ) end end end From c12d5366315955981da3d945372ac9d23843ad50 Mon Sep 17 00:00:00 2001 From: jacoyutorius Date: Tue, 9 Dec 2025 17:31:21 +0900 Subject: [PATCH 7/7] =?UTF-8?q?=F0=9F=93=9D=20aws=20login=20=E5=AF=BE?= =?UTF-8?q?=E5=BF=9C=E3=81=AE=E6=8A=80=E8=A1=93=E8=A7=A3=E8=AA=AC=E3=83=89?= =?UTF-8?q?=E3=82=AD=E3=83=A5=E3=83=A1=E3=83=B3=E3=83=88=E3=82=92=E8=BF=BD?= =?UTF-8?q?=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Aws::SharedCredentials が aws login に非対応だった問題 - profile オプションを使った解決策 - AWS SDK の認証情報チェーンの仕組み - 対応状況の比較表 --- docs/aws-login-support.md | 209 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 209 insertions(+) create mode 100644 docs/aws-login-support.md diff --git a/docs/aws-login-support.md b/docs/aws-login-support.md new file mode 100644 index 0000000..b9d3f87 --- /dev/null +++ b/docs/aws-login-support.md @@ -0,0 +1,209 @@ +# aws login 対応の技術解説 + +## 概要 + +Kagi v0.2.0 では、2024年に追加された `aws login` コマンドの認証情報に対応しました。 +このドキュメントでは、実装時に直面した問題と解決策を解説します。 + +## aws login とは + +`aws login` は AWS CLI v2.32.0 以降で利用できる新しい認証機能です: + +- **ブラウザベース認証**: AWS Management Console にブラウザでログイン +- **一時的な認証情報**: 自動的に更新される短期間の認証情報を生成 +- **OAuth 2.0 + PKCE**: セキュアな認証フロー +- **長期的なアクセスキー不要**: セキュリティリスクを削減 + +認証情報は `~/.aws/config` に以下の形式で保存されます: + +```ini +[profile my-profile] +login_session = arn:aws:sts::123456789012:assumed-role/MyRole/user +region = ap-northeast-1 +``` + +--- + +## 実装時の問題と解決策 + +### 問題1: `Aws::SharedCredentials` が `aws login` に非対応 + +#### 最初の実装 + +```ruby +# ~/.aws/credentials からプロファイルを読み込む +credentials = Aws::SharedCredentials.new(profile_name: "my-profile") + +Aws::SecretsManager::Client.new( + region: region, + credentials: credentials +) +``` + +#### 問題点 + +- `Aws::SharedCredentials` は `~/.aws/credentials` ファイルからしか認証情報を読み込めない +- `aws login` は `~/.aws/config` の `login_session` に認証情報を保存する +- そのため `Aws::SharedCredentials.new` が `nil` を返す +- 結果: `undefined method 'access_key_id' for nil` エラー + +#### エラーの再現 + +```bash +$ kagi import compal/dev --profile my-profile +エラー: 予期しないエラーが発生しました: undefined method 'access_key_id' for nil +``` + +--- + +### 問題2: `ENV['AWS_PROFILE']` の設定タイミング + +#### 2回目の実装 + +```ruby +# 環境変数でプロファイルを指定 +ENV['AWS_PROFILE'] = profile + +Aws::SecretsManager::Client.new( + region: region, + credentials: nil +) +``` + +#### 問題点 + +- 環境変数を設定しても、既に Ruby プロセスが起動しているため反映されない +- AWS SDK が環境変数を読み込むのは初期化時のみ +- 結果: 認証情報が見つからない + +#### エラーの再現 + +```bash +$ kagi import compal/dev --profile my-profile +エラー: AWS 認証情報が見つかりません。AWS Profile 'my-profile' を確認してください。 +``` + +--- + +### 解決策: `profile` オプションを直接渡す + +#### 最終的な実装 + +```ruby +def create_client(profile:, region:, debug: false) + has_env_creds = ENV['AWS_ACCESS_KEY_ID'] && ENV['AWS_SECRET_ACCESS_KEY'] + + if has_env_creds + # 環境変数が設定されている場合は明示的に Credentials を作成 + credentials = Aws::Credentials.new( + ENV['AWS_ACCESS_KEY_ID'], + ENV['AWS_SECRET_ACCESS_KEY'], + ENV['AWS_SESSION_TOKEN'] + ) + + Aws::SecretsManager::Client.new( + region: region, + credentials: credentials + ) + else + # プロファイルを指定して AWS SDK のデフォルト認証情報チェーンに任せる + Aws::SecretsManager::Client.new( + region: region, + profile: profile # ← ここがポイント! + ) + end +end +``` + +#### ポイント + +1. **環境変数がある場合**: 明示的に `Aws::Credentials` を作成 +2. **プロファイルを使う場合**: `profile: profile` オプションを渡して SDK に任せる +3. **credentials を指定しない**: SDK が自動的に適切な認証情報を見つける + +--- + +## AWS SDK の認証情報チェーン + +`profile: profile` オプションを渡すと、AWS SDK は以下の順序で認証情報を探します: + +1. **環境変数** + - `AWS_ACCESS_KEY_ID` + - `AWS_SECRET_ACCESS_KEY` + - `AWS_SESSION_TOKEN` + +2. **aws login のキャッシュ** + - `~/.aws/config` の `login_session` + +3. **aws sso login のキャッシュ** + - `~/.aws/sso/cache/` + +4. **~/.aws/credentials** + - 指定されたプロファイルの認証情報 + +5. **IAM Role** + - EC2/ECS などで実行時 + +この仕組みにより、全ての認証方式に対応できます。 + +--- + +## 対応状況の比較 + +| 実装方法 | 環境変数 | aws login | aws sso login | ~/.aws/credentials | IAM Role | +|---------|---------|-----------|---------------|-------------------|----------| +| `Aws::SharedCredentials` | ❌ | ❌ | ❌ | ✅ | ❌ | +| `ENV['AWS_PROFILE']` 設定 | ✅ | ⚠️ | ⚠️ | ⚠️ | ❌ | +| **`profile: profile` オプション** | ✅ | ✅ | ✅ | ✅ | ✅ | + +--- + +## 動作確認 + +### aws login で認証 + +```bash +# aws login でログイン +aws login + +# Kagi で使用 +kagi import my-secret --profile my-profile +``` + +### 環境変数で認証 + +```bash +# 環境変数を設定 +export AWS_ACCESS_KEY_ID="ASIA..." +export AWS_SECRET_ACCESS_KEY="..." +export AWS_SESSION_TOKEN="..." + +# Kagi で使用(--profile 不要) +kagi import my-secret +``` + +### デバッグログで確認 + +```bash +# どの認証方式が使われているか確認 +kagi import my-secret --profile my-profile --debug + +# 出力例: +# DEBUG: AWS Profile 'my-profile' を使用します (AWS SDK のデフォルト認証情報チェーン) +``` + +--- + +## まとめ + +- `Aws::SharedCredentials` は `~/.aws/credentials` 専用で、`aws login` には非対応 +- `ENV['AWS_PROFILE']` の設定はタイミングの問題で動作しない +- **`profile: profile` オプション**を使うことで、AWS SDK のデフォルト認証情報チェーンが動作し、全ての認証方式に対応できる + +この実装により、Kagi は以下の全ての認証方式をサポートします: + +- ✅ 環境変数 (`AWS_ACCESS_KEY_ID` など) +- ✅ `aws login` (新しいブラウザベース認証) +- ✅ `aws sso login` (SSO 認証) +- ✅ `~/.aws/credentials` (従来のプロファイル) +- ✅ IAM Role (EC2/ECS などで実行時)