MCP サーバーの API キーを AWS Systems Manager Parameter Store で管理してみた

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

アプリケーションサービス部の山本です。 中途社員の研修を担当しています。 が、毎日こちらが学ぶことばかりです。

はじめに

前回の記事:MCP サーバーの API キーを AWS Secrets Manager で管理してみたでは、MCP サーバーの API キーを AWS Secrets Manager で管理する方法を紹介しました。

記事を公開した後、こんな声をいただきました。

  • 「Secrets Manager は便利だけど、API キー1つに月 $0.40 はちょっと…」
  • 「うちのチームは Parameter Store で統一してるんだけど、同じことできない?」

できます。

本記事では、前回と同じアーキテクチャ(Python ラッパーで API キーを取得 → 環境変数にセット → MCP サーバーを子プロセス起動)を AWS Systems Manager Parameter Store で実現します。Standard パラメータなら 無料 です。

前回記事を読んでいない方でも理解できるよう書いていますが、「なぜ設定ファイルに API キーを書いてはいけないのか」「ラッパー方式の仕組み」については前回記事に詳しいので、併せてご覧ください。

認証の流れ

  1. AWS マネジメントコンソールにログイン
  2. aws login をターミナルで実行
  3. 認証
  4. このタイミングで、kiro を起動すると背後で中継スクリプトが実行され、 MCP サーバーが使えるようになる。

Secrets Manager と Parameter Store の比較

まず、どちらを選ぶべきか整理します。

観点 Secrets Manager Parameter Store (Standard)
コスト $0.40/シークレット/月 + API 呼び出し課金 無料(10,000 パラメータまで)
自動ローテーション ◎ Lambda 連携で自動化可能 ❌ なし(自前で実装が必要)
データ形式 JSON 文字列(複数キーを1つに格納可) 単一の値(1パラメータ = 1キー)
暗号化 デフォルトで暗号化 SecureString を選べば KMS で暗号化
バージョン管理 ◎ 自動でバージョニング ○ 履歴あり(Advanced のみ全履歴保持)
スループット 高い Standard: 40 TPS(十分)
IAM 権限 secretsmanager:GetSecretValue ssm:GetParameter + kms:Decrypt

選び方の目安:

  • 自動ローテーションが必要 → Secrets Manager
  • 複数のキーを1つにまとめたい → Secrets Manager
  • コストを抑えたい、キーが1つだけ → Parameter Store
  • 既に Parameter Store で設定管理している → Parameter Store

MCP サーバーの API キー管理という用途では、ローテーション頻度が低く、キーも1つであることが多いため、Parameter Store で十分なケースが大半です。

この記事で実現すること

flowchart LR
    Claude["Claude Desktop\n( MCP 設定ファイルの command に\nrun_mcp.py を指定)"]
    Wrapper["run_mcp.py"]
    Claude -- "ツール呼び出し・結果受信\n(stdin/stdout)" --> Wrapper
flowchart LR
    Wrapper["run_mcp.py"]
    SSM["AWS Systems Manager\nParameter Store"]
    Wrapper -- "① API キーを取得\n(GetParameter)" --> SSM
flowchart LR
    Wrapper["run_mcp.py"]
    MCP["Backlog MCP サーバー"]
    Wrapper -- "② API キーを環境変数にセット\n③ MCP サーバーを子プロセスとして起動" --> MCP
flowchart LR
    MCP["Backlog MCP サーバー"]
    Backlog["Backlog API"]
    MCP -- "環境変数の API キーで\n認証・リクエスト" --> Backlog

前回記事との違いは ① の取得先が Secrets Manager → Parameter Store に変わっただけ です。ラッパーの構造、stdin/stdout の引き継ぎ、セキュリティ上の考慮事項はすべて同じです。

前提条件

  • Python 3.9 以上
  • Node.js(MCP サーバーの実行に必要)
  • AWS CLI が設定済み(aws login または aws sso login で一時トークンを利用)
  • AWS Systems Manager Parameter Store へのアクセス権限(ssm:GetParameter
  • SecureString を使う場合は追加で kms:Decrypt 権限

必要な IAM ポリシー例

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "ssm:GetParameter",
      "Resource": "arn:aws:ssm:ap-northeast-1:123456789012:parameter/mcp/backlog/api-key"
    },
    {
      "Effect": "Allow",
      "Action": "kms:Decrypt",
      "Resource": "arn:aws:kms:ap-northeast-1:123456789012:key/your-kms-key-id"
    }
  ]
}

kms:Decrypt は SecureString を使う場合に必要です。デフォルトの aws/ssm キーを使っていれば、通常は明示的な許可なしでも動作します。

手順 1: Parameter Store にパラメータを登録する

AWS マネジメントコンソール、または AWS CLI でパラメータを作成します。SecureString を指定することで、KMS で暗号化されます。

aws ssm put-parameter \
  --name "/mcp/backlog/api-key" \
  --type "SecureString" \
  --value "あなたのBacklog APIキー" \
  --region ap-northeast-1

実行結果:

{
    "Version": 1,
    "Tier": "Standard"
}

パラメータ名の設計について

Parameter Store ではパス形式の命名が推奨されています。

/mcp/backlog/api-key      ← 本記事で使用
/mcp/github/token         ← 別の MCP サーバー用
/mcp/slack/bot-token      ← さらに別のサービス

この命名規則にしておくと、IAM ポリシーで /mcp/* をまとめて許可したり、aws ssm get-parameters-by-path で一覧取得したりできます。

手順 2: Python venv のセットアップ

前回記事と同じです。

mkdir backlog-mcp-wrapper-ssm
cd backlog-mcp-wrapper-ssm

# venv 作成
python3 -m venv venv

# boto3 をインストール
venv/bin/pip install boto3 "botocore[crt]"        # macOS / Linux
# venv\Scripts\pip install boto3 "botocore[crt]"  # Windows

手順 3: ラッパースクリプトの作成

run_mcp.py を作成します。Secrets Manager 版との差分はわずかです。

import os
import sys
import subprocess

import boto3
from botocore.exceptions import ClientError, BotoCoreError

# ============================================================
# 設定値(ご自身の環境に合わせて変更してください)
# ============================================================
# Parameter Store のパラメータ名
# SecureString で保存することを推奨
PARAMETER_NAME = "/mcp/backlog/api-key"
AWS_REGION = "ap-northeast-1"
BACKLOG_DOMAIN = "yourspace.backlog.com"

# MCP サーバーの起動コマンド
MCP_COMMAND = os.environ.get("MCP_COMMAND", "npx")
MCP_ARGS = os.environ.get(
    "MCP_ARGS",
    "-y,backlog-mcp-server,--enable-toolsets,project,wiki",
).split(",")


def get_parameter() -> str:
    """AWS Systems Manager Parameter Store からパラメータ値を取得して返す。

    SecureString の場合は WithDecryption=True で復号する。
    """
    aws_profile = os.environ.get("AWS_PROFILE", "default")

    try:
        session = boto3.session.Session(
            profile_name=aws_profile,
            region_name=AWS_REGION,
        )
        client = session.client(service_name="ssm")
        response = client.get_parameter(
            Name=PARAMETER_NAME,
            WithDecryption=True,  # SecureString でも平文で取得
        )
        return response["Parameter"]["Value"]

    except (ClientError, BotoCoreError) as e:
        # 【重要】ログは必ず stderr へ。
        # stdout に出すと Claude ⇔ MCP の JSON-RPC 通信が壊れる。
        print(
            f"[run_mcp] Parameter Store からの取得に失敗しました "
            f"(Profile: {aws_profile}, Name: {PARAMETER_NAME})",
            file=sys.stderr,
        )
        print(f"[run_mcp] エラー詳細: {e}", file=sys.stderr)
        sys.exit(1)


def main() -> None:
    # 1. Parameter Store から API キーを取得
    api_key = get_parameter()

    # 2. 子プロセス用の環境変数を構築
    env = os.environ.copy()
    env["BACKLOG_API_KEY"] = api_key
    env["BACKLOG_DOMAIN"] = os.environ.get("BACKLOG_DOMAIN", BACKLOG_DOMAIN)

    # 3. MCP サーバーを起動
    #    subprocess.run はデフォルトで親の stdin/stdout を子に引き継ぐため、
    #    Claude ⇔ MCP の JSON-RPC 通信がそのまま機能する。
    cmd = [MCP_COMMAND] + MCP_ARGS
    print(f"[run_mcp] 起動コマンド: {' '.join(cmd)}", file=sys.stderr)

    try:
        result = subprocess.run(cmd, env=env)
        sys.exit(result.returncode)
    except FileNotFoundError:
        print(
            f"[run_mcp] '{MCP_COMMAND}' コマンドが見つかりません。",
            file=sys.stderr,
        )
        sys.exit(1)
    except KeyboardInterrupt:
        pass


if __name__ == "__main__":
    main()

Secrets Manager 版との差分

変更点は実質 get_parameter() 関数だけです。

Secrets Manager 版 Parameter Store 版
import import json が必要 不要
API 呼び出し client.get_secret_value(SecretId=...) client.get_parameter(Name=..., WithDecryption=True)
戻り値の処理 json.loads(response["SecretString"]) → dict response["Parameter"]["Value"] → str
関数の戻り値型 dict(複数キー対応) str(単一の値)

Secrets Manager は1つのシークレットに JSON で複数キーを格納できるのに対し、Parameter Store は1パラメータ = 1値です。複数の API キーが必要な場合は、パラメータを複数作成して個別に取得します。

手順 4: MCP 設定ファイルの更新

前回記事と同じ構造です。command に venv の Python を、args にスクリプトのパスを指定します。

macOS / Linux

{
  "mcpServers": {
    "backlog-server": {
      "command": "/path/to/backlog-mcp-wrapper-ssm/venv/bin/python",
      "args": ["/path/to/backlog-mcp-wrapper-ssm/run_mcp.py"],
      "env": {
        "AWS_PROFILE": "my-sso-profile"
      }
    }
  }
}

Windows

{
  "mcpServers": {
    "backlog-server": {
      "command": "C:\\path\\to\\backlog-mcp-wrapper-ssm\\venv\\Scripts\\python.exe",
      "args": ["C:\\path\\to\\backlog-mcp-wrapper-ssm\\run_mcp.py"],
      "env": {
        "AWS_PROFILE": "my-sso-profile"
      }
    }
  }
}

動作確認

パラメータ取得テスト

# test_parameter.py
import os, boto3

def main():
    aws_profile = os.environ.get("AWS_PROFILE", "default")
    print(f"[テスト] 使用プロファイル: {aws_profile}")

    session = boto3.session.Session(
        profile_name=aws_profile, region_name="ap-northeast-1"
    )
    client = session.client(service_name="ssm")

    parameter_name = "/mcp/backlog/api-key"
    response = client.get_parameter(Name=parameter_name, WithDecryption=True)

    value = response["Parameter"]["Value"]
    param_type = response["Parameter"]["Type"]
    print(f"[成功] パラメータ名: {parameter_name}")
    print(f"  Type: {param_type}")
    print(f"  Value: {'*' * len(value)} ({len(value)}文字)")

if __name__ == "__main__":
    main()
venv/bin/python test_parameter.py

実行結果:

[テスト] 使用プロファイル: default
[成功] パラメータ名: /mcp/backlog/api-key
  Type: SecureString
  Value: **************************** (28文字)

Type: SecureString と表示されていれば、KMS で暗号化された状態から正しく復号できています。

E2E テスト(モック MCP サーバーを使用)

前回記事と同じモックサーバーを使います。

// mock_mcp_server.js
const readline = require('readline');

const apiKey = process.env.BACKLOG_API_KEY || '';
const domain = process.env.BACKLOG_DOMAIN || '';

// ログは stderr に(stdout は JSON-RPC 専用)
process.stderr.write(`[mock-mcp] BACKLOG_API_KEY: ${'*'.repeat(apiKey.length)}\n`);
process.stderr.write(`[mock-mcp] BACKLOG_DOMAIN: ${domain}\n`);

const rl = readline.createInterface({ input: process.stdin });

rl.on('line', (line) => {
  const request = JSON.parse(line);
  const response = {
    jsonrpc: '2.0',
    id: request.id,
    result: {
      status: 'ok',
      has_api_key: apiKey.length > 0,
      // API キーの値自体は絶対にレスポンスに含めない
    }
  };
  process.stdout.write(JSON.stringify(response) + '\n');
});
# test_e2e.py
import os, sys, json, subprocess

SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
MOCK_SERVER = os.path.join(SCRIPT_DIR, "mock_mcp_server.js")
RUN_MCP = os.path.join(SCRIPT_DIR, "run_mcp.py")
VENV_PYTHON = os.path.join(SCRIPT_DIR, "venv", "bin", "python")

def test_via_wrapper():
    env = os.environ.copy()
    env["AWS_PROFILE"] = os.environ.get("AWS_PROFILE", "default")
    env["MCP_COMMAND"] = "node"
    env["MCP_ARGS"] = MOCK_SERVER

    request = json.dumps({"jsonrpc": "2.0", "id": 1, "method": "test"}) + "\n"

    proc = subprocess.run(
        [VENV_PYTHON, RUN_MCP],
        input=request, capture_output=True, text=True, timeout=15,
        env=env,
    )

    if proc.returncode != 0:
        print(f"❌ 失敗: プロセスが異常終了 (code={proc.returncode})", file=sys.stderr)
        print(f"  stderr: {proc.stderr}", file=sys.stderr)
        sys.exit(1)

    response = json.loads(proc.stdout.strip())
    assert response["result"]["has_api_key"] is True, "API キーが渡されていません"
    print("✅ 成功: Parameter Store → 環境変数 → MCP サーバーの連携が正常")

if __name__ == "__main__":
    test_via_wrapper()
venv/bin/python test_e2e.py

実行結果:

✅ 成功: Parameter Store → 環境変数 → MCP サーバーの連携が正常

複数の API キーが必要な場合

Secrets Manager では1つのシークレットに JSON で複数キーを格納できましたが、Parameter Store は1パラメータ = 1値です。複数キーが必要な場合は、パス階層を活用します。

# 複数パラメータを登録
aws ssm put-parameter --name "/mcp/backlog/api-key" --type SecureString --value "..."
aws ssm put-parameter --name "/mcp/backlog/domain" --type String --value "yourspace.backlog.com"

スクリプト側では get_parameters_by_path でまとめて取得できます。

def get_parameters_by_path(path_prefix: str) -> dict:
    """指定パス配下のパラメータをまとめて取得して dict で返す。"""
    aws_profile = os.environ.get("AWS_PROFILE", "default")

    session = boto3.session.Session(
        profile_name=aws_profile,
        region_name=AWS_REGION,
    )
    client = session.client(service_name="ssm")
    response = client.get_parameters_by_path(
        Path=path_prefix,
        WithDecryption=True,
        Recursive=True,
    )

    # "/mcp/backlog/api-key" → "api-key" のようにキー名だけ取り出す
    params = {}
    for param in response["Parameters"]:
        key = param["Name"].split("/")[-1]
        params[key] = param["Value"]
    return params

この方法なら、Secrets Manager の JSON 格納と同じ感覚で複数キーを扱えます。ただし、IAM ポリシーで ssm:GetParametersByPath の許可が追加で必要です。

Secrets Manager 版からの移行

既に前回記事の Secrets Manager 版を使っている方が Parameter Store 版に移行する手順です。

# 1. Secrets Manager から値を取り出して Parameter Store に登録
API_KEY=$(aws secretsmanager get-secret-value \
  --secret-id mcp/backlog-keys \
  --query 'SecretString' --output text \
  --profile my-sso-profile \
  --region ap-northeast-1 \
  | python3 -c 'import sys,json; print(json.loads(sys.stdin.read())["BACKLOG_API_KEY"])')

aws ssm put-parameter \
  --name "/mcp/backlog/api-key" \
  --type "SecureString" \
  --value "$API_KEY" \
  --profile my-sso-profile \
  --region ap-northeast-1

# 2. 新しい venv をセットアップ
mkdir backlog-mcp-wrapper-ssm && cd backlog-mcp-wrapper-ssm
python3 -m venv venv
venv/bin/pip install boto3 "botocore[crt]"

# 3. run_mcp.py を配置(本記事のコードをコピー)

# 4. MCP 設定ファイルのパスを更新
#    command: .../backlog-mcp-wrapper/venv/bin/python
#    → command: .../backlog-mcp-wrapper-ssm/venv/bin/python

# 5. 動作確認後、不要になった Secrets Manager のシークレットを削除(任意)
# aws secretsmanager delete-secret --secret-id mcp/backlog-keys --force-delete-without-recovery

この方式の限界(前回記事の再掲 + 追記)

前回記事で述べた限界はそのまま当てはまります。

  • 環境変数自体のリスクは残る — 最終的に API キーは子プロセスの環境変数に入る
  • 守るべきものが移動しただけ — AWS 認証情報が漏洩したら同じ(ただし SSO の一時トークンなのでリスクの性質が異なる)

Parameter Store 固有の注意点を追記します。

SecureString を使わないと意味がない

--type String で登録すると、Parameter Store 上で平文保存されます。API キーには必ず --type SecureString を指定してください。

自動ローテーションがない

Secrets Manager と違い、Parameter Store にはビルトインのローテーション機能がありません。API キーを定期的に更新したい場合は、自前でスクリプトを書くか、Secrets Manager を使ってください。

スループット制限

Standard パラメータは 40 TPS(1秒あたり40リクエスト)の制限があります。MCP サーバーの起動時に1回取得するだけなので、通常は問題になりません。ただし、大量の MCP サーバーを同時起動するような構成では注意が必要です。

まとめ

方法 セキュリティ コスト 手軽さ
args に直書き ❌ 危険 無料
env に直書き △ ファイルにキーが残る 無料
Secrets Manager + ラッパー $0.40/月〜
Parameter Store + ラッパー 無料

Parameter Store 版は Secrets Manager 版と同じセキュリティレベルを 無料で 実現できます。自動ローテーションが不要で、キーの数が少ない場合は、こちらの方がシンプルでおすすめです。

前回記事と合わせて、チームの要件に合った方を選んでいただければと思います。


前回記事: MCP サーバーの API キーを AWS Secrets Manager で管理してみた

余談

富士山の上の方でも桜が咲いていました。

山本 哲也 (記事一覧)

多分インフラエンジニアです。データ分析に興味あります。

山を走るのが趣味です。今年は 100 マイルレース完走します。