KINTO Tech Blog
AWS

【脱コンソールポチポチ】多環境AWS Parameter StoreをYAML×GitHub Actionsで一元管理する仕組みを作った

Cover Image for 【脱コンソールポチポチ】多環境AWS Parameter StoreをYAML×GitHub Actionsで一元管理する仕組みを作った

この記事は KINTOテクノロジーズアドベントカレンダー2025 の10日目の記事です🎅🎄

目次

はじめに
この記事の要約
作ったもの
特徴
前提条件
運用上の工夫
アーキテクチャ
実装のポイント
使い方
導入効果
まとめ

はじめに

こんにちは。KINTOテクノロジーズの共通サービス開発グループのエンジニア、宮下です。

AWSなどを使う現在の開発環境は、簡単に増やしたり減らしたりできる反面、環境の数が増えていきがちです。
我々の開発環境も、dev、dev2、stg、stg2...stg5、ins、prodのように、とても多くなってしまっています。

そのため、AWS Systems Manager Parameter Storeで管理する値も、環境数 × パラメータ数の組み合わせでどんどん増えていき、ケアレスミスが多発していました。

例えば以下のようなミスです。

  • devのParameter Storeは最新化したけれど、stgのパラメータを更新し忘れていた
  • KEY, VALUEは正しいけれど、タグを入れ忘れていた
  • デプロイに失敗するので調査したら、新規追加のパラメータを登録し忘れていたのが原因だった
  • devからstgへ手作業でコピペする際、環境ごとに変えるべき値をそのままコピペしてしまった

また、現在のParameter Storeの値がどうなっているかも分かりづらく、ブラウザでいちいち各環境に入って確認するのが面倒で、つい後回しになりがちでした。その結果、よりケアレスミスが起きる悪循環に陥っていました。

そこで、「ローカルのYAMLファイルに各環境のパラメータを集約し、そのファイルとAWSを同期する」という方針で自動化の仕組みを作りました。今回はそのアイデアを紹介します。

この記事の要約

  • 10環境以上 × 50以上のパラメータ のAWS Parameter Store管理が煩雑すぎたので自動化した
  • YAMLで全パラメータを可視化 + GitHub Actionsでワンクリック同期
  • 更新漏れやケアレスミスをゼロにし、面倒くさい機械的なコピペ作業を無くした

作ったもの

以下の3つを組み合わせてこの仕組みを構築しました:

  1. YAMLファイル - 全環境のParameter Storeの値を一元管理
  2. Pythonスクリプト - YAMLとAWS間の同期処理
  3. GitHub Actions - ワンクリックで全環境に反映

特徴

1. YAML可視化

従来は各環境のParameter Storeの値を確認するために、AWSコンソールに各環境ごとにアカウントを切り替えて何度もログインする必要がありました。

今は、リポジトリにある1つのYAMLファイルを開けば、全環境の全パラメータが一覧できます。

parameters:
  # 環境ごとに値が異なるパラメータ
  - key: api/endpoint
    description: "APIエンドポイント"
    environment_values:
      dev:  "https://dev-api.example.com"
      stg:  "https://stg-api.example.com"
      prod: "https://prod-api.example.com"

  # 全環境で共通の値
  - key: app/timeout
    description: "タイムアウト設定(秒)"
    default_value: "30"

  # 機密情報(SecureString)
  # 値はGitHub Secretsから環境変数経由で取得(例: SSM__DB__PASSWORD)
  - key: db/password
    description: "データベースパスワード"
    type: "SecureString"

メリット:

  • どの環境でどの値を使っているか一目瞭然
  • Git管理できるので変更履歴も追える
  • PRレビューで値の間違いを事前に防げる

2. ワンクリック同期

GitHub Actionsのワークフローを手動実行するだけで、全環境のParameter Storeが自動的にYAMLの内容と同期されます。

マトリクス・ストラテジーにより、複数環境(dev, stg, prod)が並列で実行されるため、環境が増えても実行時間はほぼ変わりません。

AWSコンソールで環境を切り替えながらポチポチする必要がなくなりました。

前提条件

この仕組みは以下の前提で動作します。

  • AWS CLIがGitHub Actionsランナー上で利用できること
  • Parameter Storeへの ssm:GetParametersByPath, ssm:PutParameter, ssm:AddTagsToResource 権限があること
  • GitHub ActionsからAWSにアクセスできること(Access Keyもしくは OIDCなど)
  • Python 3.11 + pyyaml が利用できること

運用上の工夫

自動化で便利になる一方、誤操作のリスクも考慮が必要です。我々のチームでは以下の工夫をしています。

SecureStringの操作権限を絞る

GitHub Secretsを編集できる人を限定し、機密情報を扱える人を最小限にしています。

本番環境への反映はワークフローを分けて承認制に

本番環境のParameter Store更新時は、本番環境専用のワークフローを使い、ワークフローの中でSlackで承認ステップを挟む運用にしています。承認依頼時には更新対象のパラメータ一覧がSlackに表示されるため、「何が変わるのか」を確認してから承認できます。これにより、うっかり本番を更新してしまう事故を防いでいます。

アーキテクチャ

実装のポイント

ディレクトリ構成

$ tree .github
.github
├── aws-params.yml
├── scripts
│   ├── aws_param_common.py
│   └── update_aws_params.py
└── workflows
    └── sync-parameters.yml

YAML設計

全環境のパラメータを .github/aws-params.yml に集約しています(YAMLの例は「特徴」セクションを参照)。

SecureStringの扱い

DBのパスワードなどの機密情報をYAMLにベタ書きするのはセキュリティ上NGです。
そこで、「YAMLにはキーの定義のみ」「実体(値)はGitHub Secrets」 という役割分担を行いました。

Pythonスクリプト側で、YAMLの定義を見て type: SecureString ならば、対応する環境変数を読みに行く設計にしています。

命名規則:

YAMLのkey: db/password
→ 環境変数名: SSM__DB__PASSWORD

環境ごとにSecureStringの値を分ける

DBパスワードなどは環境ごとに異なる値を使うことが多いです。GitHub Actionsの
Environments 機能を使えば、環境ごとに異なるSecretsを設定できます。

設定手順:

  1. GitHubリポジトリの Settings → Environments で環境を作成(dev, stg, prod
  2. 各環境のSecretsに SSM__DB__PASSWORD などを登録(値は環境ごとに異なる)
  3. ワークフローで environment: ${{ matrix.env }} を指定

これにより、DEV環境ではDEV用のDBパスワード、STG環境ではSTG用のDBパスワードが自動的に使われます。

Github_Actions_の環境変数
Github_Actions_devの環境変数

補足:Parameter Store vs Secrets Manager

「機密情報ならSecrets Managerでは?」と思う方もいるかもしれません。使い分けの目安は以下の通りです:

Parameter Store (SecureString) Secrets Manager
料金 標準パラメータは無料 $0.40/シークレット/月
ローテーション 手動 自動ローテーション可能
向いているケース APIキーなど更新頻度が低いもの DBパスワードの自動ローテーションが必要な場合

多くのケースではParameter Store(SecureString)で十分で、Secrets Managerは「RDSパスワードの自動ローテーション」が必要な場合に検討してください。

Pythonスクリプト構成

aws_param_common.py - 共通機能

#!/usr/bin/env python3
"""AWS Parameter Store 共通処理"""

import os
import sys
import json
import subprocess
from typing import Dict, Any, Tuple
import yaml


def get_env_name() -> str:
    """環境名を取得"""
    env = os.environ.get("ENV_NAME")
    if not env:
        print("エラー: ENV_NAME 環境変数が設定されていません")
        sys.exit(1)
    return env


def get_prefix(env: str) -> str:
    """環境に応じたプレフィックスを返す"""
    return f"/{env}/app/config/"


def load_yaml_config() -> Tuple[Dict[str, Any], set]:
    """YAMLファイルを読み込む"""
    yaml_path = os.path.join(os.path.dirname(__file__), "..", "aws-params.yml")
    with open(yaml_path, "r", encoding="utf-8") as f:
        config = yaml.safe_load(f)
    yaml_keys = {param["key"] for param in config.get("parameters", [])}
    return config, yaml_keys


def get_existing_params(env: str) -> Dict[str, Dict[str, Any]]:
    """AWS SSMから既存のパラメータを取得(ページネーション対応)"""
    prefix = get_prefix(env)
    existing_params = {}
    next_token = None

    while True:
        cmd = [
            "aws", "ssm", "get-parameters-by-path",
            "--path", prefix,
            "--recursive",
            "--with-decryption",
            "--output", "json"
        ]
        if next_token:
            cmd.extend(["--next-token", next_token])

        try:
            result = subprocess.run(cmd, check=True, capture_output=True, text=True)
            data = json.loads(result.stdout)
            params_data = data.get("Parameters", [])
        except subprocess.CalledProcessError as e:
            print(f"警告: パラメータの取得に失敗しました: {e.stderr}")
            return {}

        for param in params_data:
            key = param["Name"].replace(prefix, "")
            existing_params[key] = {
                "value": param["Value"],
                "type": param["Type"],
                "version": param.get("Version", 1)
            }

        next_token = data.get("NextToken")
        if not next_token:
            break

    return existing_params


def get_param_value(param: Dict[str, Any], env: str) -> str | None:
    """パラメータの値を取得(SecureStringは環境変数、それ以外はYAMLの値を使用)"""
    # SecureStringの場合は環境変数から取得
    if param.get("type") == "SecureString":
        env_var_name = "SSM__" + param["key"].upper().replace("/", "__")
        value = os.environ.get(env_var_name)
        if not value:
            print(f"警告: SecureString {param['key']} の環境変数 {env_var_name} が未設定")
            return None
        return value

    # 環境固有の値
    env_values = param.get("environment_values", {})
    if env in env_values:
        return str(env_values[env])

    # デフォルト値
    if "default_value" in param:
        return str(param["default_value"])
    return None


def validate_param(param: Dict[str, Any], env: str) -> Tuple[bool, str, Dict[str, Any] | None]:
    """パラメータのバリデーション"""
    key = param.get("key")
    if not key:
        return False, "keyが定義されていません", None

    value = get_param_value(param, env)
    if value is None:
        return False, f"{key}: 環境 {env} の値が定義されていません", None

    param_info = {
        "key": key,
        "value": value,
        "type": param.get("type", "String"),
        "description": param.get("description", "")
    }
    return True, "", param_info


def update_parameter(param_info: Dict[str, Any], env: str) -> bool:
    """パラメータを更新"""
    prefix = get_prefix(env)
    full_name = prefix + param_info["key"]

    cmd = [
        "aws", "ssm", "put-parameter",
        "--name", full_name,
        "--value", param_info["value"],
        "--type", param_info["type"],
        "--overwrite"
    ]
    if param_info.get("description"):
        cmd.extend(["--description", param_info["description"]])

    try:
        subprocess.run(cmd, check=True, capture_output=True, text=True)
        add_tags(full_name, env)  # タグを追加
        return True
    except subprocess.CalledProcessError as e:
        print(f"エラー: {param_info['key']} の更新に失敗: {e.stderr}")
        return False


def add_tags(parameter_name: str, env: str) -> bool:
    """パラメータにタグを追加"""
    cmd = [
        "aws", "ssm", "add-tags-to-resource",
        "--resource-type", "Parameter",
        "--resource-id", parameter_name,
        "--tags",
        f"Key=Environment,Value={env}",
        "Key=SID,Value=backend-api"
    ]
    try:
        subprocess.run(cmd, check=True, capture_output=True, text=True)
        return True
    except subprocess.CalledProcessError as e:
        print(f"警告: タグの追加に失敗: {e.stderr}")
        return False

ポイント:

  • get_existing_params: ページネーション対応で50件以上のパラメータも取得可能
  • get_param_value: SecureStringは環境変数から、通常パラメータはYAMLから値を取得
  • update_parameter: パラメータ更新後に add_tags を呼び出してタグを付与

タグについて

パラメータ作成時に、自動でタグを付与します。タグはAWSコンソールでの検索やコスト管理に便利なだけでなく、システムによってはタグがないとパラメータを読み込めない場合もあります。

タグ 説明
Environment dev, stg, prod 実行時の環境名が自動で入る
SID backend-api サービス識別子(自分のサービス名に置き換えて使用)

update_aws_params.py - 更新スクリプト

#!/usr/bin/env python3
"""AWS Parameter Store 更新スクリプト"""

import sys
import aws_param_common as common


def update_parameters():
    """パラメータを更新し、結果をレポートする"""
    env = common.get_env_name()
    print(f"=== 環境: {env} ===")
    print(f"プレフィックス: {common.get_prefix(env)}")
    print()

    config, yaml_keys = common.load_yaml_config()
    existing_params = common.get_existing_params(env)
    print(f"既存パラメータ数: {len(existing_params)}")
    print()

    updated_params = []
    skipped_params = []
    failed_params = []

    for param in config.get("parameters", []):
        is_valid, error_msg, param_info = common.validate_param(param, env)

        if not is_valid:
            print(f"[スキップ] {error_msg}")
            continue

        param_key = param_info["key"]
        value = param_info["value"]

        # 既存の値と比較
        if param_key in existing_params:
            if existing_params[param_key]["value"] == value:
                print(f"[スキップ] {param_key}: 値に変更なし")
                skipped_params.append(param_key)
                continue
            print(f"[更新] {param_key}: 値を更新します")
        else:
            print(f"[新規] {param_key}: 新規パラメータを追加します")

        # パラメータを更新
        success = common.update_parameter(param_info, env)
        if success:
            updated_params.append(param_key)
            print(f"  ✓ 完了")
        else:
            failed_params.append(param_key)
            print(f"  ✗ 失敗")

    # 結果サマリー
    print()
    print("=== 結果サマリー ===")
    print(f"更新: {len(updated_params)} 件")
    print(f"スキップ(変更なし): {len(skipped_params)} 件")
    print(f"失敗: {len(failed_params)} 件")

    if failed_params:
        print()
        print("失敗したパラメータ:")
        for key in failed_params:
            print(f"  - {key}")
        sys.exit(1)

    print()
    print("✓ 正常終了")


if __name__ == "__main__":
    update_parameters()

ポイント:

  • 値が変わっていないパラメータはスキップ(無駄な更新を防ぐ)
  • 更新結果を統計情報として出力
  • 失敗時は終了コード1で終了

GitHub Actionsワークフロー

name: Sync AWS Parameter Store

on:
  workflow_dispatch:  # 手動実行

jobs:
  sync-parameters:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        env: [dev, stg, prod]
    environment: ${{ matrix.env }}  # 環境ごとのSecretsを使用

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Setup Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.11'

      - name: Install dependencies
        run: pip install pyyaml

      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: ap-northeast-1

      - name: Sync Parameters
        env:
          ENV_NAME: ${{ matrix.env }}
          # SecureString用(環境ごとのGitHub Secretsから取得)
          SSM__DB__PASSWORD: ${{ secrets.SSM__DB__PASSWORD }}
          SSM__API__SECRET_KEY: ${{ secrets.SSM__API__SECRET_KEY }}
        run: |
          cd .github/scripts
          python update_aws_params.py

ポイント:

  • strategy.matrix で複数環境を並列実行
  • environment: ${{ matrix.env }} で環境ごとのSecretsを使用(devとprodで異なるDBパスワードなど)
  • SecureStringの値は環境変数経由でスクリプトに渡す
  • 値に変更がないパラメータは自動的にスキップされる

使い方

1. パラメータの追加・変更

.github/aws-params.yml を編集してPRを出すだけです。

parameters:
  # 新しいパラメータを追加
  - key: feature/enable_payment_v2
    description: "新決済システムの有効化"
    environment_values:
      dev:  "true"
      stg:  "false"
      prod:  "false"

2. 全環境への反映

  1. GitHub Actionsページを開く
  2. Sync AWS Parameter Store を選択
  3. Run workflow ボタンをクリック
  4. 全環境に並列で反映される

3. SecureStringパラメータの追加

  1. YAMLに定義を追加:
- key: payment/api_key
  description: "決済APIキー"
  type: "SecureString"
  1. GitHub Environmentsに値を登録:

Settings → Environments → 各環境(dev, stg, prod)のSecretsに登録

Secret名: SSM__PAYMENT__API_KEY
値: (環境ごとに異なる実際の値)
  1. ワークフローファイルの環境変数セクションに追加:
env:
  SSM__PAYMENT__API_KEY: ${{ secrets.SSM__PAYMENT__API_KEY }}

4. 実演

  1. GitHub Actions画面から今回作ったアクションを選んで、起動します。
    Github_Actionsの選択と実行

  2. アクションが正常終了したことを確認します。各環境が並列で動作した事がわかります。
    Github_Actionsの動作結果

  3. AWSのパラメータストア画面を開いて確認してみます。パラメータが登録されています。成功です。
    パラメータストアの登録結果

導入効果

具体的な効果

  • 作業時間: 環境数 × 5分 → ワンクリック(10環境なら50分削減)
  • 更新漏れ: 月数回発生 → ゼロ
  • 確認作業: AWSコンソールを開く → YAMLを見るだけ

まとめ

クラウド時代の「環境が増えすぎ問題」は、多くの現場で直面している課題だと思います。

今回紹介したアイデアのポイントは:

  1. YAML可視化 - 全環境のパラメータを1ファイルで管理
  2. ワンクリック同期 - GitHub Actionsで自動反映
  3. SecureString対応 - 機密情報も安全に管理

特別な技術は使っておらず、GitHub Actions + Python + AWS CLI だけで実現できます。

Parameter Storeの管理で困っている方、環境が増えて運用が大変になっている方の参考になれば幸いです。

最後までお読みいただき、ありがとうございました🙇‍♂️

Facebook

関連記事 | Related Posts

We are hiring!

【クラウドエンジニア】Cloud Infrastructure G/東京・大阪・福岡

KINTO Tech BlogWantedlyストーリーCloud InfrastructureグループについてAWSを主としたクラウドインフラの設計、構築、運用を主に担当しています。

【クラウドプラットフォームエンジニア】プラットフォームG/東京・大阪・福岡

プラットフォームグループについてAWS を中心とするインフラ上で稼働するアプリケーション運用改善のサポートを担当しています。

イベント情報