AWS Service Catalog 1年本番運用して気づいた「セルフサービス化の現実」

「誰がこのリソース作った?」の地獄から抜け出すためにService Catalogを導入して1年。テンプレート管理や権限設計でハマった落とし穴と、チームに定着させるまでのリアルを書きました。

「誰がこのリソース作った?」の地獄から始まった

1年半前、うちのチームは本当にひどい状態だった。開発者が思い思いにAWSコンソールをポチポチしてEC2やRDSを立てまくり、タグはバラバラ、セキュリティグループの設定もまちまち。AWSの「誰が作ったかわからない」リソースで月200万円溶けてた話は笑えない話として他所でも聞くけど、うちも同じ状況だった。

そこで導入を決めたのがAWS Service Catalogだ。正直、最初は「CloudFormationのラッパーに過ぎないんじゃないか」と懐疑的だったんだけど、1年運用してみると想像より深いツールだった。同時に想像よりしんどい部分もあった。その両方を正直に書いていく。

Service Catalogの全体像と2026年時点のアップデート

Service Catalogは「ITサービスのカタログ化」というコンセプトのサービスで、管理者が承認済みのCloudFormationテンプレートを「製品(Product)」として登録し、開発者がそのカタログから必要なリソースをセルフサービスで起動できる仕組みだ。

2025〜2026年にかけていくつか重要なアップデートがあった。

機能2024年以前2026年時点
Terraform対応限定的(External Engine経由)Terraform Cloud/Enterprise統合が安定化
AWS CDK統合非公式workaroundCDKで生成したCFnテンプレートの直接利用が推奨パターンに
ServiceNow連携プラグイン経由Connector for ServiceNow v3でより安定
AppRegistry統合ベータ段階Application Managerと深く統合、タグ戦略との連携が強化
AI推奨機能なしAmazon Q DeveloperがCatalog製品の推奨を補助

Terraform対応については、正直まだ完全にスムーズじゃない部分もある。うちはCDK→CloudFormationのパイプラインを採用している。CDK Pipelines 2026年版の記事でも触れたが、CDKで書いたコンストラクトをCfnテンプレートとしてService Catalogに登録するワークフローが今のところ一番スムーズだと感じている。

実際に構築したアーキテクチャ

graph TB
  subgraph "管理アカウント"
    SC["AWS Service Catalog\n製品ポートフォリオ"]
    CFN["CloudFormation\nテンプレート"]
    S3["S3\nテンプレートバケット"]
    IAM["IAM\n起動ロール"]
    SC --> CFN
    CFN --> S3
    SC --> IAM
  end

  subgraph "開発アカウント"
    subgraph "VPC (10.0.0.0/16)"
      subgraph "AZ-a"
        EC2a["EC2 インスタンス\n(カタログ製品)"]  
        RDSa["RDS Primary\n(カタログ製品)"]
      end
      subgraph "AZ-b"
        EC2b["EC2 インスタンス\n(カタログ製品)"]
        RDSb["RDS Standby\n(カタログ製品)"]
      end
      SGallow["Security Group\n(テンプレート管理)"]
    end
    Dev1["開発者A"]
    Dev2["開発者B"]
    Dev1 -->|"カタログから起動"| SC
    Dev2 -->|"カタログから起動"| SC
    SC -->|"共有ポートフォリオ"| EC2a
    SC -->|"共有ポートフォリオ"| RDSa
  end

  subgraph "本番アカウント"
    subgraph "VPC (10.1.0.0/16)"
      subgraph "AZ-a"
        ProdEC2a["EC2 Auto Scaling"]
        ProdRDS["Aurora Cluster"]
      end
      subgraph "AZ-b"
        ProdEC2b["EC2 Auto Scaling"]
      end
    end
    Ops["Ops チーム"]
    Ops -->|"承認フロー経由"| SC
    SC -->|"共有ポートフォリオ"| ProdEC2a
    SC -->|"共有ポートフォリオ"| ProdRDS
  end

  subgraph "CI/CDパイプライン"
    Git["GitHub\nCDKコード"]
    Pipeline["CodePipeline"]
    Build["CodeBuild\ncdk synth"]
    Git --> Pipeline --> Build --> SC
  end

ポイントは管理アカウントに製品を集約して、Organizations経由で各アカウントに共有ポートフォリオを配布している点だ。AWS Organizations セキュリティ設計の記事でも書いたが、マルチアカウント前提の設計を最初から意識しておかないと後で地獄を見る。

テンプレート管理でハマった3つの落とし穴

1. バージョン管理を甘く見てた

最初は「製品を更新したら全員が自動的に新バージョンを使う」と思い込んでいた。実際は違う。Service Catalogでは製品の更新をしても、既存のプロビジョニングされたアイテムは古いバージョンのままだ。開発者が意識的に「更新」の操作をしないといけない。

これが意外と曲者で、気づいたら開発環境が古いバージョンのセキュリティグループ設定で動いてた、みたいなことが2回あった。今は以下のような通知の仕組みを入れている:

import boto3
import json
from datetime import datetime, timedelta

def check_outdated_provisioned_products(account_id: str) -> list[dict]:
    """
    古いバージョンのService Catalog製品を使っているリソースを検出する
    """
    sc_client = boto3.client('servicecatalog')
    outdated = []
    
    # プロビジョニング済みアイテムを一覧取得
    paginator = sc_client.get_paginator('scan_provisioned_products')
    for page in paginator.paginate(
        AccessLevelFilter={'Key': 'Account', 'Value': 'self'}
    ):
        for product in page['ProvisionedProducts']:
            if product['Status'] != 'AVAILABLE':
                continue
            
            # 製品の最新バージョンを取得
            product_id = product['ProductId']
            try:
                artifact_detail = sc_client.describe_product(
                    Id=product_id
                )
                latest_artifact = artifact_detail['ProvisioningArtifacts'][-1]
                current_artifact_id = product.get('ProvisioningArtifactId', '')
                
                if current_artifact_id != latest_artifact['Id']:
                    outdated.append({
                        'name': product['Name'],
                        'product_id': product_id,
                        'current_version': current_artifact_id,
                        'latest_version': latest_artifact['Id'],
                        'last_updated': product.get('UpdatedTime', 'unknown'),
                        'owner': product.get('Tags', {}).get('Owner', 'unknown')
                    })
            except Exception as e:
                print(f"Error checking {product_id}: {e}")
    
    return outdated

# Lambda関数として定期実行、Slackに通知
def lambda_handler(event, context):
    outdated_products = check_outdated_provisioned_products('current')
    
    if not outdated_products:
        return {'statusCode': 200, 'body': 'All products are up to date'}
    
    # Slack通知(SNS経由)
    sns_client = boto3.client('sns')
    message = f"⚠️ Service Catalog: {len(outdated_products)}個の古いバージョンを検出\n"
    for p in outdated_products:
        message += f"- {p['name']} (Owner: {p['owner']})\n"
    
    sns_client.publish(
        TopicArn='arn:aws:sns:ap-northeast-1:123456789012:sc-alerts',
        Message=message,
        Subject='Service Catalog バージョン更新通知'
    )
    
    return {'statusCode': 200, 'body': json.dumps(outdated_products)}

これをEventBridgeで毎週月曜朝に動かすようにした。地味に便利だし、「更新してください」と口頭で言い続けるより断然効く。

2. IAMロールの設計で3回やり直した

Service Catalogの権限モデルはちょっとクセがある。「起動ロール(Launch Role)」と「エンドユーザーロール」の2層になっているんだけど、最初これを混同してハマった。

  • エンドユーザーロール: 開発者がService Catalogのコンソールを操作する権限
  • 起動ロール: 実際にCloudFormationがリソースを作成するときに使う権限

開発者に「EC2を起動できる権限を直接渡さず」に、起動ロール経由でEC2を作らせる、という設計が肝だ。これがIaC的なガバナンスの核心で、開発者は「カタログにあるものしか作れない」という制約を自然に実現できる。個人的にはここがService Catalogの一番おもしろい部分だと思っている。

# 起動ロールのポリシー例(CloudFormation Stackが使用)
AWSTemplateFormatVersion: '2010-09-09'
Description: Service Catalog Launch Role

Resources:
  ServiceCatalogLaunchRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: ServiceCatalogLaunchRole
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: servicecatalog.amazonaws.com
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - !Ref LaunchPolicy

  LaunchPolicy:
    Type: AWS::IAM::ManagedPolicy
    Properties:
      ManagedPolicyName: ServiceCatalogLaunchPolicy
      PolicyDocument:
        Version: '2012-10-17'
        Statement:
          # EC2の起動はタグ必須条件付き
          - Effect: Allow
            Action:
              - ec2:RunInstances
              - ec2:CreateTags
            Resource: '*'
            Condition:
              StringEquals:
                aws:RequestedRegion: ap-northeast-1
              # カタログ経由のリソースには必ずこのタグが付く
              StringLike:
                aws:RequestTag/ManagedBy: 'ServiceCatalog'
          # RDSは事前定義のサイズのみ
          - Effect: Allow
            Action:
              - rds:CreateDBInstance
            Resource: '*'
            Condition:
              StringEquals:
                rds:DatabaseClass:
                  - db.t3.medium
                  - db.t3.large
                  - db.r6g.large
          # CFnスタック操作
          - Effect: Allow
            Action:
              - cloudformation:CreateStack
              - cloudformation:UpdateStack
              - cloudformation:DeleteStack
              - cloudformation:DescribeStacks
            Resource: 'arn:aws:cloudformation:*:*:stack/SC-*'

3. テンプレートのパラメータ設計が甘かった

最初は「開発者が自由に設定できるように」と思ってパラメータをたくさん用意した。結果、毎回「どれを選べばいいですか?」という質問が来て本末転倒だった。自由にしすぎると人は迷う、というのは今となっては当たり前の話なんだけど、やってみて初めてわかった。

今は「Allowed Values」を徹底的に使って選択肢を絞り込んでいる。

Parameters:
  EnvironmentType:
    Type: String
    Default: dev
    AllowedValues:
      - dev
      - staging
      - prod
    Description: |
      環境タイプを選択してください。
      dev: t3.small / staging: t3.medium / prod: r6g.large が自動選択されます
  
  ApplicationName:
    Type: String
    MinLength: 3
    MaxLength: 20
    AllowedPattern: '^[a-z][a-z0-9-]+$'
    Description: アプリケーション名(小文字英数字とハイフンのみ)

Conditions:
  IsProduction: !Equals [!Ref EnvironmentType, prod]
  IsStaging: !Equals [!Ref EnvironmentType, staging]

Mappings:
  InstanceTypeMap:
    dev:
      InstanceType: t3.small
      MultiAZ: false
    staging:
      InstanceType: t3.medium
      MultiAZ: false
    prod:
      InstanceType: r6g.large
      MultiAZ: true

こうすると開発者は「dev」「staging」「prod」を選ぶだけで、インスタンスタイプやマルチAZ設定は自動で決まる。自由度を下げることがセルフサービス化の質を上げるというのが、1年運用して得た一番の気づきかもしれない。

運用効率の変化(実測データ)

「リソース作成依頼の対応」にかかっていた月間工数が、導入前の72時間から12ヶ月後には14時間まで下がった。数字で見るとなかなかドラマチックだ。

xychart-beta
  title "Service Catalog導入前後の運用時間比較(月次・時間)"
  x-axis ["導入前", "導入1ヶ月", "導入3ヶ月", "導入6ヶ月", "導入12ヶ月"]
  y-axis "月間工数(時間)" 0 --> 80
  bar [72, 45, 32, 20, 14]
  line [72, 45, 32, 20, 14]

もちろんService Catalog自体の運用コスト(テンプレート更新・権限管理など)は新たに発生しているが、それでもトータルでは大幅な削減になっている。

正直、最初の2〜3ヶ月は逆に工数が増えた時期もあった。テンプレート作成・レビュー・テストのサイクルを確立するまでの投資期間として、ある程度の覚悟は必要だ。グラフを見ると「導入1ヶ月」でもそれなりに減っているが、あの時期は裏でかなり残業してたのが真相だったりする。

承認フローの実装パターン

本番環境へのデプロイには承認ステップを入れている。Service CatalogにはネイティブのSNS通知機能があり、これを使ってSlack承認フローを実装した。

sequenceDiagram
  participant Dev as 開発者
  participant SC as Service Catalog
  participant SNS as SNS Topic
  participant Lambda as 承認Lambda
  participant Slack as Slack
  participant Ops as Opsチーム
  participant CFn as CloudFormation

  Dev->>SC: カタログから製品を起動
  SC->>SNS: 起動リクエスト通知
  SNS->>Lambda: トリガー
  Lambda->>Slack: 承認リクエスト投稿(承認/却下ボタン付き)
  Ops->>Slack: 承認ボタンをクリック
  Slack->>Lambda: Webhookで通知
  Lambda->>SC: 承認API呼び出し
  SC->>CFn: スタック作成開始
  CFn->>Dev: 完了通知

ただ、全リソースに承認フローを入れると開発者の体験が悪くなる。今は以下の3カテゴリに絞っている:

  • 本番環境全般
  • RDS・ElastiCache等のデータストア
  • NAT Gateway・Load Balancer等のコスト影響が大きいリソース

開発環境のEC2/ECSは自動承認で即時起動できるようにした。このバランスがわりと大事で、ここを間違えると「面倒なので直接コンソールを使う」という逆行現象が起きる。せっかく整備したカタログを誰も使わない、というのが一番虚しい事態なので。

Terraform・CDKとの棲み分けをどう考えているか

これは正直、好みが分かれるところかもしれない。うちの現在のスタンスを整理するとこうなる:

ツール使うシーン
Service Catalog開発者がセルフサービスで繰り返し起動する標準化リソース(EC2, RDS, ECS Task, S3 Bucket等)
AWS CDK一度作ったら変えない基盤インフラ(VPC, Transit Gateway, Organizations設定等)
Terraform他クラウドやSaaSとの統合が必要な部分、Datadogリソース等
SSM Automation運用系の自動化(パッチ適用、バックアップ取得等)

SSM Automationを1年運用して気づいた設計の勘どころでも触れているが、SSM AutomationはService Catalogと組み合わせると「リソース作成後の初期設定」を自動化できて相性がいい。地味に便利な組み合わせなので試してみてほしい。

よくある失敗:ServiceNow・Jiraとの連携

うちの組織はJiraを使っているので、Service CatalogのリクエストをJiraチケットと連携させようと試みた。これが思ったより大変だった。

2026年時点でService CatalogとJiraを直接統合する公式コネクタはないので、EventBridge→Lambda→Jira APIという自前実装が必要になる。実装自体はできたが、Jiraのプロジェクト設定によって挙動が変わったり、JiraのWebhook認証の仕様変更があったりで、3ヶ月ほど維持コストがかかり続けた。

今思えば、Slackベースの軽量な承認フローで十分だったかもしれない。ITSMツールとのヘビーな統合は、組織規模が大きくて統制要件が厳しいケースでないとオーバーエンジニアリングになりやすい。皆さんの組織ではどうしてます?

まとめ

1年間Service Catalogを本番運用してわかったことを整理するとこうなる:

  1. セルフサービス化の本質は「制約」:パラメータを絞り、選択肢を限定することで開発者の自由度を適切に管理する。自由度を与えすぎると混乱する
  2. バージョン管理は仕組みで担保する:古いバージョンを放置する開発者への通知自動化は最初から入れておくべきだった
  3. IAMの2層モデルを最初に理解する:起動ロールとエンドユーザーロールの違いを把握していないと権限設計が迷走する
  4. 承認フローは絞り込んで設計する:全リソースに承認を入れると形骸化する。本当に制御したいリソースにだけかける
  5. CDKとの組み合わせが2026年の現実解:CDKで生成したCloudFormationテンプレートをService Catalogに登録するワークフローが最もメンテしやすい

まず1つの「標準的なEC2構成」だけカタログ化して試してみるのがおすすめだ。全部一気にやろうとすると絶対途中で詰まる。スモールスタートで成功体験を作ってから拡張していくのが、うちのチームでうまくいったパターンだった。

Service Catalogの導入を検討しているチームの参考になれば。チームの規模感や統制要件によって最適解はかなり変わってくるので、何かあればコメントで教えてください。

U

Untanbaby

ソフトウェアエンジニア|AWS / クラウドアーキテクチャ / DevOps

10年以上のIT実務経験をもとに、現場で使える技術情報を発信しています。 記事の誤りや改善点があればお問い合わせからお気軽にご連絡ください。

関連記事