SSM Parameter Store と Secrets Manager の使い分け:クロススタック参照と機密管理の実装

著者: なぎゆー公開日: 2026-05-25読了目安: 約 11
AWSSSMSecrets ManagerCDK

AWS の SSM Parameter Store と Secrets Manager を、モノレポ × マルチサービス構成の実装に即して使い分ける指針を整理。Parameter Store はスタック間のリソース参照(CfnOutput / ImportValue の代替)、Secrets Manager は API キーや鍵ペアの機密管理という役割分担を、CDK のコードとともに解説します。

はじめに

AWS で「値をコードの外に出す」手段として、SSM Parameter Store と Secrets Manager はよく並べて語られます。どちらも「キーと値を保管して実行時に取り出す」点は共通していますが、本プラットフォームでは両者を明確に役割分担して使っています。

  • SSM Parameter Store … スタックが生成した非機密のリソース識別子を、別のスタックへ受け渡す
  • Secrets Manager … API キーや鍵ペアといった、外部由来の機密値を実行ロールへ安全に渡す

「機密かどうか」で選んでいる、と言ってもいいのですが、本質は 「スタックが生成する識別子か/外部から持ち込む秘密か」 という区別です。本記事では、この使い分けが実装でどう現れているかを CDK のコードとともに整理します。

結論:何をどちらに入れているか

保管対象 保管先 性質
VPC ID・サブネット ID・ALB ARN・ECS クラスタ名・ECR URI・Distribution ID Parameter Store スタックが生成するリソース識別子
OpenAI API キー・VAPID キーペア・dev 用 IAM 認証情報 Secrets Manager 外部から持ち込む機密値

Parameter Store には機密でない識別子しか入れていないため、SecureString も使っていません。逆に Secrets Manager には、コードにも CloudFormation テンプレートにも残したくない値だけを入れています。

Parameter Store:スタック間参照を疎結合にする

なぜ ImportValue ではなく SSM なのか

モノレポでマルチサービスを CDK 管理していると、「VPC スタックが作った VPC ID を、各サービスの ALB スタックから参照したい」というクロススタック参照が頻発します。

素朴な手段は CloudFormation の Export と Fn.importValue です。実際、初期は次のように Export 名を定数化して使っていました。

import * as cdk from 'aws-cdk-lib';
import { EXPORTS } from '../shared/libs/utils/exports';

// CloudFormation の Export を参照
const vpcId = cdk.Fn.importValue(EXPORTS.VPC_ID('dev')); // → 'nagiyu-dev-vpc-id'

ただし Fn.importValue には厄介な性質があります。Export された値が他スタックから参照されている間、その Export を変更・削除できません。VPC スタックを更新したいだけなのに、参照しているスタックが依存ロックになって cdk deploy が止まる、という事態が起きます。スタック同士が硬く結合してしまうのです。

そこで本プラットフォームでは、共通リソースのクロススタック参照を SSM Parameter Store 経由に切り替えています。アーキテクチャ方針としても「共通 Cluster と共通 VPC は SSM Parameter Store 経由で参照する。スタック間の直接依存は持たない」と定めています。SSM 経由なら、生成側と消費側はパラメータ名という文字列でしか繋がらず、デプロイ順序や更新の自由度が大きく上がります。

パラメータ名は定数で一元管理する

参照キーが文字列になる以上、typo が一番怖いところです。そこでパラメータ名は定数オブジェクトに集約しています。

export const SSM_PARAMETERS = {
  VPC_ID: (env: Environment) => `/nagiyu/shared/${env}/vpc/id`,
  PUBLIC_SUBNET_IDS: (env: Environment) => `/nagiyu/shared/${env}/vpc/public-subnet-ids`,
  ALB_ARN: (env: Environment) => `/nagiyu/root/${env}/alb/arn`,
  ECS_CLUSTER_NAME: (env: Environment) => `/nagiyu/root/${env}/ecs/cluster-name`,
  // ...
} as const;

命名は /nagiyu/{scope}/{env}/{resource} の階層構造に統一しています。scope(shared / root / 各サービス)と env(dev / prod)が必ず入るので、パラメータを一覧したときに「どのスタックが・どの環境向けに」出力したものかが一目で分かります。

生成側:StringParameter で書き出す

リソースを作ったスタックが、その識別子を SSM に書き出します。

import * as ssm from 'aws-cdk-lib/aws-ssm';

new ssm.StringParameter(this, 'ClusterNameParam', {
  parameterName: SSM_PARAMETERS.ECS_CLUSTER_NAME(environment),
  stringValue: cluster.clusterName,
});

消費側:valueForStringParameter で読み込む

参照する側は valueForStringParameter で同じキーを読みます。

const vpcId = ssm.StringParameter.valueForStringParameter(this, SSM_PARAMETERS.VPC_ID(environment));

const vpc = ec2.Vpc.fromLookup(this, 'ExistingVpc', { vpcId });

これだけで、VPC スタックと ALB スタックの間に CloudFormation 上の依存関係を作らずに、値を受け渡せます。SSM_PARAMETERS という同じ定数を両側で使うので、書き出すキーと読むキーがズレることもありません。

Secrets Manager:機密値を実行ロールへ渡す

一方、API キーや鍵ペアのように「コードにもテンプレートにも残したくない値」は Secrets Manager に入れます。Parameter Store のリソース識別子とは性質がまったく違い、こちらは外部から持ち込む秘密です。

PLACEHOLDER で作り、実値は手動で入れる

ポイントは、CDK では値を持たせないことです。初回は PLACEHOLDER でシークレットの「箱」だけを作り、実際の値は AWS Console から後で上書きします。

import * as cdk from 'aws-cdk-lib';
import * as secretsmanager from 'aws-cdk-lib/aws-secretsmanager';

// 初回は PLACEHOLDER で作成し、実値は Console から上書きする
this.openAiApiKeySecret = new secretsmanager.Secret(this, 'OpenAiApiKeySecret', {
  secretName: `nagiyu-stock-tracker-openai-api-key-${environment}`,
  description: 'OpenAI API key for stock analysis batch processing',
  secretObjectValue: {
    apiKey: cdk.SecretValue.unsafePlainText('PLACEHOLDER'),
  },
});

VAPID キーペアも同じ作りです。

this.vapidSecret = new secretsmanager.Secret(this, 'VapidSecret', {
  secretName: `nagiyu-stock-tracker-vapid-${environment}`,
  description: 'VAPID key pair for Web Push notifications',
  secretObjectValue: {
    publicKey: cdk.SecretValue.unsafePlainText('PLACEHOLDER'),
    privateKey: cdk.SecretValue.unsafePlainText('PLACEHOLDER'),
  },
});

generateSecretString を使わない理由

Secrets Manager には generateSecretString でランダム値を自動生成する機能があります。便利な機能ですが、ここでは使っていません。保管したい値が「自分で決められる秘密」ではないからです。

  • VAPID キーは web-push generate-vapid-keys で生成したペアを貼り付ける
  • OpenAI API キーは OpenAI 側で払い出された文字列をそのまま入れる

どちらも「ランダムに生成してよい値」ではなく、外部で確定した値を持ち込むだけなので、箱だけ CDK で用意して中身は手で入れる、という運用が一番素直です。

ローテーションは使っていない

Secrets Manager の目玉機能である自動ローテーションも、現状は使っていません。ローテーションが効くのは RDS の認証情報のように「AWS 側で更新できる秘密」ですが、本プラットフォームのデータストアは DynamoDB が中心で、ローテーション対象になる DB クレデンシャルが存在しないためです。「Secrets Manager を使う = ローテーションする」ではない、という点は実装方針として割り切っています。

アクセスは実行ロールに ARN 限定で付与する

シークレットの読み取りは、まず IAM で「どのロールがどのシークレットを読めるか」を絞ります。バッチ Lambda の実行ロールには、対象シークレットの ARN だけを指定して GetSecretValue を付与します。

new iam.PolicyStatement({
  sid: 'SecretsManagerVapidAccess',
  effect: iam.Effect.ALLOW,
  actions: ['secretsmanager:GetSecretValue'],
  resources: [props.vapidSecret.secretArn], // ワイルドカードではなく ARN 限定
});

このポリシーは ManagedPolicy として定義し、バッチ Lambda の実行ロールと、ローカル開発用 IAM ユーザーの両方にアタッチしています。開発者が手元で動かすときも本番 Lambda とまったく同じ権限になるため、「ローカルでは動いたのに本番で権限不足」というデプロイ後の事故を防げます。

設計上のポイント

SSM 採用の主目的は「疎結合」

Parameter Store を「安い設定置き場」として捉えると本質を外します。少なくともこのプラットフォームでの主目的は、クロススタック参照から CloudFormation の依存ロックを外し、スタックを独立して更新・削除できるようにすることです。

Parameter Store に機密は置かない

SSM に入れているのは VPC ID や ARN といった非機密の識別子だけなので、SecureString も使っていません。機密は Secrets Manager 側に寄せる、と役割をはっきり分けることで、「どこに何があるか」が迷子になりません。

機密はコードに焼き込まず、箱だけ作る

Secrets Manager 側は PLACEHOLDER で箱を作り、実値は Console から入れる運用です。CDK のコードにもテンプレートにも秘密が残らないので、リポジトリや CloudFormation のドリフト履歴から漏れる心配がありません。

まとめ

SSM Parameter Store と Secrets Manager は競合ではなく、保管する値の性質で役割分担しています。スタックが生成する非機密の識別子は Parameter Store でスタック間に疎結合に渡し、外部から持ち込む機密値は Secrets Manager に箱だけ作って実値は手で入れる——この線引きが、本プラットフォームでの基本方針です。

「Parameter Store = 安い設定置き場」「Secrets Manager = 自動生成とローテーション」という一般的なイメージとは少し違う使い方ですが、依存ロックの回避と機密の非焼き込みという、運用で本当に効く観点から選ぶと、自然とこの形に落ち着きます。