さらに詳しく

メタデータ管理(S3 + DynamoDB)

● S3を拡張機能(VSIXファイル)の永続ストレージとして使用します。各拡張機能は <Publisher>/<ExtensionName>/<Version>/<ファイル名>.vsix のパス構造でS3バケットに保存します(例:my-bucket/ms-python/python/2022.14.0/ms-python.python-2022.14.0.vsix)。これによりパブリッシャー名やバージョンでファイルを整理できます。メタデータJSONを各VSIXに添付することも可能ですが、DynamoDBを併用することで検索性能と更新処理を向上させます 。DynamoDBには拡張機能のメタデータ(拡張機能ID、表示名、バージョン、説明文、カテゴリ等)を保持し、検索クエリに素早く応答します。S3は大規模になると更新分の一覧取得にコストがかかるため(日時指定での差分一覧APIがなく全リストが必要 )、DynamoDBでインデックス化しておく方が効率的です。DynamoDBのテーブル設計は例えば「PK: 拡張機能ID(Publisher.Extension)、SK: バージョン」とし、1つの拡張機能につき複数バージョンの項目を持てるようにします。最新バージョンや基本情報は重複して格納し、検索時は拡張機能名や説明文に対する部分一致でDynamoDBをScanまたはインデックス検索します(小規模な件数ならScanでも実用十分)。メタデータ更新時は、DynamoDBをキーアクセスすることで特定拡張の情報を即座に取得・更新でき、検索用の属性(例えば小文字正規化した名前、タグ一覧など)も持たせます。結果として**「S3+DynamoDB」**でファイル本体と検索用データを分離し、双方の利点を活かしたメタデータ管理を行います 。なお、コードマーケットプレイス(code-marketplace)でも拡張機能をファイルストレージから読み込みAPI提供する構成が採られており 、将来的な拡張としてデータベース併用による検索高速化が言及されています 。今回の設計でも同様のアプローチをとります。

検索機能(API設計)

コードサーバ(code-server)やVS Codeからの拡張検索要求に応答する検索APIをLambda上に実装します。Coder MarketplaceやOpen VSXと互換性のあるAPIとすることで、code-server側の設定で当該エンドポイントをカスタム拡張マーケットプレイスとして認識させることが可能です 。具体的には、**Visual Studio Marketplaceの拡張検索API (ExtensionQuery)**と互換のJSONリクエスト/レスポンス形式を採用します。
• リクエスト形式: HTTP POSTでエンドポイント例/api/extensionqueryに対し、Content-Type: application/jsonヘッダ付きでボディに検索クエリをJSONで渡します 。クエリJSONはVisual Studio CodeのギャラリーAPIと同様の構造で、filters配列に検索条件(criteria)を含みます。例えば拡張機能名キーワード「Python」で検索する場合、次のようなJSONになります :
{
  "filters": [{
    "criteria": [
      { "filterType": 8, "value": "Microsoft.VisualStudio.Code" },
      { "filterType": 10, "value": "Python" }
    ],
    "pageNumber": 1,
    "pageSize": 50,
    "sortBy": 0,
    "sortOrder": 0
  }],
  "assetTypes": [],
  "flags": 0
}
上記例ではfilterType:8で対象製品をVS Codeに絞り(code-serverでもこの値を使用)、filterType:10に検索文字列を指定しています 。特定の拡張機能ID(publisher.extension形式)を直接指定する検索ではfilterType:7にそのIDを渡します 。Lambda側ではこれらフィルタ内容を解析し、DynamoDBに対して該当拡張機能をクエリします。例えば、filterType:7があればDynamoDBの主キー取得で該当アイテム(およびバージョン一覧)を取得し、filterType:10(テキスト検索)があれば拡張名や説明に該当文字列を含む項目をScanまたはインデックス照会で抽出します。ページングやソートも可能な範囲でサポートします(必要に応じてDynamoDBのLimit指定やクライアント側ソートを利用)。
• レスポンス形式: VS Code拡張ギャラリーAPIの3.0-preview.1に準拠したJSONを返します。具体的には、results配列下にextensionsのリストを含み、各拡張機能オブジェクトに基本情報とバージョン情報を格納します。例として、拡張機能「Python」のレスポンス構造を簡略化すると以下のようになります:
{
  "results": [{
    "extensions": [{
      "extensionId": "ms-python.python",
      "publisher": { "publisherName": "ms-python" },
      "extensionName": "python",
      "displayName": "Python",
      "shortDescription": "Python language support",
      "versions": [{
        "version": "2022.14.0",
        "files": [
          {
            "assetType": "Microsoft.VisualStudio.Services.VSIXPackage",
            "source": "https://<配信ドメイン>/ms-python/python/2022.14.0/ms-python.python-2022.14.0.vsix"
          },
          {
            "assetType": "Microsoft.VisualStudio.Services.Icons.Default",
            "source": "https://<配信ドメイン>/ms-python/python/2022.14.0/icon.png"
          }
        ]
      }]
    }]
  }]
}
上記のように各バージョンごとにfiles配列でアセット一覧(VSIXパッケージ本体やアイコン画像など)を提供します。特にVSIX本体はassetType: "Microsoft.VisualStudio.Services.VSIXPackage"としてダウンロードURLをsourceに含めます 。これによりcode-server側はこのURLを直接取得して拡張機能をインストールできます 。なお表示名や説明、アイコンURLなども含めることで、code-serverの拡張パネルに拡張機能名や説明文を表示できます。以上のようにAPIリクエスト/レスポンス構造を既存Marketplaceと合わせることで、code-serverやOpen VSX対応エディタからシームレスに検索が可能となります。

インストール機能(VSIXファイル配信)

拡張機能のインストール要求に対しては、クライアント(code-server)が前述の検索結果で得たVSIXファイルURLにアクセスすることでインストール用パッケージをダウンロードします。サーバレス構成におけるVSIX配信方法として、**「S3署名付きURL」を返す方法と「CloudFront経由で配信」**する方法が考えられます。それぞれの比較と実装方法は以下の通りです。
• S3署名付きURL方式: Lambdaが一時的に有効な署名付きURL(Presigned URL)を発行し、これをダウンロードリンクとしてクライアントに提供します。検索APIのレスポンス内のVSIXファイルsourceにこの署名付きURLを直接埋め込む実装も可能です。その場合、ユーザが拡張をインストールする際にcode-serverは即座にそのURLにアクセスし、S3から直接VSIXを取得します。署名付きURLはAWSの資格情報でリクエストに署名することで生成され、指定した有効期限を過ぎると無効になります。利点としては実装がシンプルで、Lambda上でboto3のgenerate_presigned_urlを呼ぶだけでURLが得られる点、またURL毎に有効期限やアクセス権限を細かく制御できる点があります。一方で欠点として、URLのドメインがS3のエンドポイント(例:s3.amazonaws.com)となるためcode-serverから見ると検索APIとは別ドメインになります。code-serverは既定で未知のドメインへのアクセス時に警告を出す可能性がありますが、product.jsonの信頼ドメイン設定に当該S3ドメインを追加することで対処できます 。あるいは検索APIでresourceUrlTemplateを適切に設定しておけばcode-serverが許可する場合もあります。セキュリティ面では署名付きURL自体にアクセス制御情報が含まれており、URLの秘密性が保たれる限り安全にS3非公開オブジェクトへアクセスできます 。実装上はLambda関数内で必要に応じ都度URLを発行しますが、検索結果一覧の全拡張に署名URLを埋め込むと未インストールのものにも発行するため、インストール要求時に別エンドポイントで発行する方法も検討できます。例えば検索結果ではsourceにプレースホルダURL(または単にCloudFrontパス)を返し、実際のダウンロード時にクライアントが/api/download?extensionId=X@versionのようなAPIを叩いて署名URLを取得しリダイレクトする方式です。しかしcode-serverの実装上、検索結果に含まれるURLを直接取りに行く挙動のため、簡便さを優先して検索レスポンスに署名URLを含める設計とします。
• CloudFront配信方式: AWS CloudFrontを用いてS3上のVSIXファイルをCDN経由で公開配信する方法です。まずCloudFrontのオリジンにS3バケットを設定し、拡張機能ファイルへのリクエストをCloudFrontが受け付けるようにします。S3バケットは非公開とし、Origin Access Identity(OAI)またはOrigin Access Control(OAC)を使ってCloudFrontからのアクセスのみ許可する構成にします。こうすることで、クライアントはCloudFront経由以外でS3に直接アクセスできません。CloudFront配信のURLを検索結果sourceに含めれば、ユーザはCloudFrontのドメイン(カスタムドメインを割り当て可能)からVSIXをダウンロードします。利点は同一ドメインでのサービス提供やキャッシュによる性能向上です。例えば検索APIと同じドメイン(パス違い)でCloudFront配信を行えば、code-server側で別ドメイン警告の問題もありません。またCloudFrontはエッジキャッシュを備えるため、地理的に分散した利用や多数ユーザが同じ拡張を取得する場合にS3直アクセスよりも効率的です。欠点は設定の複雑さとコストで、CloudFrontディストリビューションの作成・証明書設定(カスタムドメイン利用時)やキャッシュ無効化の考慮が必要です。またアクセス制限を強化する場合、CloudFront自体の署名付きURL/クッキー機能を用いることもできます 。これはあらかじめCloudFront用のキーを発行しLambdaで署名する方式ですが、実装がやや高度になります。多くの社内利用ケースではCloudFrontのURLを知る人のみアクセス可能としつつネットワークで保護すれば十分なため、署名無しでもよいでしょう(OAIにより直リンクは防がれている前提)。総合すると、利用者が限定された環境や小規模運用ならS3署名付きURLでシンプルに実装し、大規模展開やドメイン統一を図りたい場合はCloudFront経由を採用するのが適切です。今回の提案では、CloudFront方式を推奨します。理由はcode-serverとのドメイン統一が容易になる点、および将来的に利用者が増えても配信性能とスケーラビリティを確保できる点です。検索APIはLambda経由ですが、VSIX配信はCloudFront+S3にオフロードすることでLambdaの負荷を低減できます。

APIエンドポイント設計(Lambda URL vs API Gateway)

検索APIおよび(必要なら)ダウンロードAPIを外部公開するにあたり、Lambda Function URLを直接利用するか、API Gateway経由で公開するかの選択があります。それぞれの特徴を比較し、最適なエンドポイント構成を検討します。
• Lambda Function URL: 各Lambda関数に対して個別に割り当てるHTTPSエンドポイントです。設定が簡便であり、追加コストが発生しない点がメリットです 。例えば検索用Lambdaに対しFunction URLを有効化すれば、即座にhttps://<ランダムID>.lambda-url.<region>.on.aws/のURLでリクエストを受け付けられます。シンプルなマイクロサービス(単一機能の公開)に適しており、認証も必要なければAuthType: NONEでそのまま利用できます 。一方で、複数のエンドポイントを持つAPIを構築する場合にはFunction URL方式だとエンドポイントが分散しがちです。例えば検索とダウンロードで別々のURLになり、ドメイン統合するにはRoute53やリバースプロキシでパス振り分けする必要があります。また認証・認可やAPIキー管理、レート制限等の高度な制御はFunction URL単体ではサポートされません(IAM認証か無認証のみ) 。ログやモニタリングも個別LambdaのCloudWatchログを見る形となります。
• API Gateway (HTTP API): API Gatewayを用いる場合、複数のLambdaを単一ドメインのパスで統合し、まとまったRESTful APIを提供できます 。例えば/api/extensionqueryは検索Lambda、/api/downloadはダウンロードLambdaに振り分ける、といったルーティングを定義できます。API Gateway自体がHTTPSエンドポイントとなり(例: https://abcd1234.execute-api.ap-northeast-1.amazonaws.com/prod/)、カスタムドメインをマッピングすれば人に読みやすい独自ドメインでも公開可能です。CORS設定もAPI Gateway側でまとめて制御できます。さらに、認証(CognitoやJWTオーソライザ)、リクエスト検証、レスポンス変換、キャッシュ、レート制限、ログ記録など多彩な機能を利用できるのが利点です 。企業内利用で将来的に認証を追加したり、API利用状況を可視化する可能性があるならば、API Gatewayの方が拡張性があります。またステージ管理により開発/本番環境を分離できるのもメリットです。デメリットは追加のランタイムコスト(リクエスト数に応じた料金)と若干のレイテンシ増加ですが、小規模なAPIでは無視できる範囲です。
• 最適な選択肢: 上記を踏まえ、本件ではAPI Gateway経由でエンドポイントを提供する構成を提案します。理由は、エクステンション検索とVSIX配信で複数のAPIエンドポイントが想定され、それらを一元的に管理したいためです。API Gatewayを使うことで、/api/*配下に検索やヘルスチェック等のAPIを集約し、将来の機能追加(例:拡張一覧取得APIやカテゴリフィルタAPI)も容易になります。また社内システム認証や追加のセキュリティ要件が発生した場合でもAPI Gatewayレイヤーで対処できます。一方、Lambda URLは迅速な試験や低コスト運用に適しているため、開発・検証段階ではFunction URLで実装し、本番展開時にAPI Gatewayに切り替える運用も考えられます。いずれの場合も、code-server側ではEXTENSIONS_GALLERYのserviceUrlを当該エンドポイントに向けることで動作します 。(なおitemUrlについては拡張詳細ページ用ですが、本システムではWeb閲覧画面を提供しないためダミーエンドポイントを返すか、"not supported"固定レスポンスのLambdaを用意します 。)

メタデータ更新の仕組み

拡張機能(VSIX)ファイルがS3にアップロードされた際、自動でメタデータを登録/更新する仕組みを構築します。S3はオブジェクトの作成・削除イベントを通知できるため、対象バケットに**Lambdaトリガー(ObjectCreated, ObjectRemoved)**を設定します 。これにより、新しいVSIXファイルの追加や削除時に、特定のLambda関数が起動します。更新Lambdaでは以下の処理を行います :
• メタデータ抽出: イベントからS3バケット名・キー(パス)を取得し、必要に応じてS3から該当VSIXファイルを取得(GetObject)します。VSIXはZIP形式のため、Lambda内で一時領域にダウンロードして解凍し、extension/package.json(拡張機能のマニフェストファイル)を読み込みます。このマニフェストに拡張機能の識別子(publisher.name), バージョン, 表示名, 概要説明, カテゴリなど検索用フィールドが含まれています。それらを取り出し、DynamoDBに保存するItemを構築します。キーとなる拡張機能IDはpublisher.nameを結合したものとし(例: ms-python.python)、バージョンも含めて項目を作成します。既に同じ拡張IDの項目が存在する場合はバージョン一覧の更新(新バージョンの追加)となります。DynamoDBではPutItem時に拡張ID+バージョンの複合キーで一意に扱うか、あるいは拡張IDごとに1アイテムでversions配列を持つ構造でも構いません。設計次第ですが、本提案では拡張IDをパーティションキー、バージョンをソートキーとしてテーブルに格納し、各バージョンを独立項目として扱います。これにより、ある拡張の全バージョンをQueryで取得可能で、最新フラグなどの管理も容易です。加えて、表示名や説明文などは最新版の項目から取得しつつ、必要に応じ全バージョン共通で更新します。
• DynamoDBへの登録/更新: DynamoDBテーブルに対し、上記メタデータをPutItemまたはUpdateItemします。新規拡張の場合は新たなパーティションを作成し、既存拡張の新バージョン追加なら既存項目に追加、もしくは新たなソートキー項目を追加します。UpdateItem時に条件式を用いて「指定バージョンが未登録の場合のみ追加」することで冪等性を担保します。成功後、ログに拡張IDとバージョンを出力し、検索APIで即座に検索・インストール可能となります。
• 削除イベント対応: S3オブジェクト削除(ObjectRemoved)イベントも受け取り、対応する拡張のメタデータをDynamoDBから削除します(対象バージョンの項目をDeleteItemし、最後のバージョンだった場合はパーティション自体削除)。もっとも、誤削除防止のため即時には消さずフラグを立て非表示とする運用も考えられます。簡易には削除イベント時にDynamoDB項目を削除し、検索結果に出さないようにします。

このようにS3イベント駆動でメタデータの追加・削除を自動化することで、管理者がS3にVSIXを配置するだけでマーケットプレイスの情報が更新されます 。手動でのメタデータ入力は不要であり、ファイルストレージと検索データストアの同期が保証されます 。
また、定期的な整合性チェックバッチ(LambdaやAWS Glueなど)を設け、S3上の全拡張とDynamoDBの項目を照合して不整合がないか確認するとより信頼性が高まります。例えばDynamoDBにあるがS3に実ファイルがないデータや、その逆(ファイルあるのにDynamoに登録漏れ)などを検知できます。しかし基本的にはS3イベントでリアルタイム連携されるため、大きな問題は起きにくい構成です。

実装コード例(Lambda関数・S3構成・IAM)

最後に、上記の機能を実現するための主要コンポーネントの実装例を示します。Lambda関数はPythonで記述し、AWS SDKであるboto3を使用します。またインフラは主にAWSマネジメントコンソールやIaCツール(CloudFormation/Terraform等)で設定しますが、ここではコード例に重点を置きます。

🔹 検索API Lambda 関数(Python)

以下は検索リクエストを処理し、DynamoDBから結果を取得してMarketplace互換のJSONを返すLambda関数の例です(簡易なロジックを示しています)。API Gatewayを前提とし、イベントはAPI GWのプロキシ統合フォーマットを想定しています。
import json, boto3, os
dynamo = boto3.resource('dynamodb')
table = dynamo.Table(os.environ['DYNAMO_TABLE_NAME'])

def lambda_handler(event, context):
    # リクエストボディの取得
    body = json.loads(event.get('body', '{}'))
    # 検索クエリ抽出(filterTypeごとに処理)
    keyword = None
    exact_ext_id = None
    filters = body.get('filters', [])
    for f in filters:
        for criteria in f.get('criteria', []):
            ftype = criteria.get('filterType')
            val = criteria.get('value', '')
            if ftype == 7:
                # 拡張機能ID (publisher.name)
                exact_ext_id = val.lower()
            elif ftype == 10:
                # キーワード検索
                keyword = val.lower()
    results = []
    if exact_ext_id:
        # 拡張IDでの精確検索
        resp = table.get_item(Key={'ExtensionId': exact_ext_id})
        if 'Item' in resp:
            results.append(resp['Item'])
    elif keyword:
        # キーワード検索: 全アイテムをスキャンし、NameやDescriptionに含むものを抽出
        scan_resp = table.scan()
        for item in scan_resp.get('Items', []):
            name = item.get('ExtensionName', '').lower()
            disp = item.get('DisplayName', '').lower()
            desc = item.get('Description', '').lower()
            if keyword in name or keyword in disp or keyword in desc:
                results.append(item)
    else:
        # フィルタ指定なし: 全件 or 適切に処理
        scan_resp = table.scan()
        results = scan_resp.get('Items', [])
    # DynamoDBから取得したItemをAPIレスポンス形式に整形
    extensions_list = []
    for item in results:
        ext_id = item['ExtensionId']  # "publisher.name"
        publisher, name = ext_id.split('.', 1)
        ext_obj = {
            "extensionId": ext_id,
            "publisher": {"publisherName": publisher},
            "extensionName": name,
            "displayName": item.get('DisplayName', name),
            "shortDescription": item.get('Description', ''),
            "versions": []
        }
        # DynamoDBにバージョンリストを持たせている場合
        versions = item.get('Versions', [])  # 例えば["2022.14.0", ...]
        for ver in versions:
            # VSIX配信URLを生成(CloudFront URL使用例)
            file_url = f"{os.environ['FILE_BASE_URL']}/{publisher}/{name}/{ver}/{ext_id}-{ver}.vsix"
            ver_obj = {
                "version": ver,
                "files": [
                    {
                        "assetType": "Microsoft.VisualStudio.Services.VSIXPackage",
                        "source": file_url
                    }
                ]
            }
            ext_obj["versions"].append(ver_obj)
        extensions_list.append(ext_obj)
    response_body = {"results": [{"extensions": extensions_list}]}
    return {
        "statusCode": 200,
        "headers": {"Content-Type": "application/json"},
        "body": json.dumps(response_body)
    }
※上記では簡略化のためエラーハンドリングやページング、省略されたフィールドなどは実装していません。また、DynamoDBのスキャンは小規模データを想定しています。大規模な場合はFilterExpressionや**全文検索サービス(Amazon OpenSearch等)**の利用も検討します。

🔹 VSIX配信Lambda 関数(Python)※署名URL方式を採用する場合

CloudFrontを使用する場合はLambdaで特別な処理を行う必要はなく、上記検索Lambda内でfile_urlとしてCloudFrontのパスを組み立てています。一方、Presigned URL方式で実装する場合の例も示します。この関数は、クエリ文字列やパスで指定された拡張機能IDとバージョンに対応するS3上のオブジェクトに対し、期間限定のダウンロードURLを発行して返します(HTTPリダイレクトやJSONでURL返却など運用に応じて使い分けます)。
import os, boto3, urllib.parse
s3 = boto3.client('s3')
bucket_name = os.environ['EXTENSION_BUCKET']

def lambda_handler(event, context):
    # パス or クエリから拡張IDとバージョンを取得(例: /download?extensionId=ms-python.python&version=2022.14.0)
    params = event.get('queryStringParameters', {}) or {}
    ext_id = params.get('extensionId')
    version = params.get('version')
    if not ext_id or not version:
        return {"statusCode": 400, "body": "Missing parameters"}
    publisher, name = ext_id.split('.', 1)
    object_key = f"{publisher}/{name}/{version}/{ext_id}-{version}.vsix"
    # 署名付きURLを生成(有効期限60秒)
    try:
        url = s3.generate_presigned_url(
            ClientMethod='get_object',
            Params={'Bucket': bucket_name, 'Key': object_key},
            ExpiresIn=60
        )
    except Exception as e:
        return {"statusCode": 500, "body": "Error generating URL"}
    # リダイレクトで署名URLに転送する場合
    return {
        "statusCode": 302,
        "headers": {"Location": url}
    }
上記のようにLambdaから一時URLを発行し、クライアントにリダイレクトします。検索APIで直接署名URLを埋め込む場合は、この処理を検索Lambda内で行い、sourceにurlを入れる実装となります。

🔹 メタデータ更新Lambda 関数(Python)

最後に、S3アップロードイベントでトリガーされるメタデータ更新用Lambdaの例です。VSIXファイルから必要な情報を抽出し、DynamoDBに登録します(シンプルさのためエラーチェック等は省略)。ここではzipファイルを扱うため、Lambda関数にあらかじめzipfileモジュールをインポートしています。
import boto3, json, os, zipfile, tempfile
s3 = boto3.client('s3')
dynamo = boto3.resource('dynamodb')
table = dynamo.Table(os.environ['DYNAMO_TABLE_NAME'])

def lambda_handler(event, context):
    # S3イベントからバケット名とオブジェクトキー取得
    for record in event.get('Records', []):
        event_name = record['eventName']  # e.g. "ObjectCreated:Put" or "ObjectRemoved:Delete"
        bucket = record['s3']['bucket']['name']
        key = record['s3']['object']['key']
        if event_name.startswith("ObjectCreated"):
            # VSIXが追加された場合
            # 一時ファイルにダウンロード
            tmp_path = os.path.join(tempfile.gettempdir(), "ext.vsix")
            s3.download_file(bucket, key, tmp_path)
            # VSIX (ZIP)を開いてpackage.json読む
            with zipfile.ZipFile(tmp_path, 'r') as z:
                data = None
                try:
                    with z.open('extension/package.json') as f:
                        data = json.load(f)
                except KeyError:
                    # package.jsonがない(VSIX形式でない)場合はスキップ
                    continue
            if data:
                publisher = data.get('publisher', '')
                name = data.get('name', '')
                version = data.get('version', '')
                display_name = data.get('displayName', name)
                desc = data.get('description', '')
                ext_id = f"{publisher}.{name}"
                # DynamoDBにアップsert(既存拡張なら更新、なければ新規)
                # ここではversions配列をテーブルで管理している想定
                table.update_item(
                    Key={'ExtensionId': ext_id},
                    UpdateExpression="SET #disp = :disp, #desc = :desc, #vers = list_append(if_not_exists(#vers, :empty_list), :new_ver)",
                    ExpressionAttributeNames={
                        '#disp': 'DisplayName', '#desc': 'Description', '#vers': 'Versions'
                    },
                    ExpressionAttributeValues={
                        ':disp': display_name,
                        ':desc': desc,
                        ':empty_list': [],
                        ':new_ver': [version]
                    }
                )
        elif event_name.startswith("ObjectRemoved"):
            # VSIXが削除された場合
            # 対応するDynamoDBデータを削除
            # キーから拡張IDとバージョンを推定(キーは "<publisher>/<name>/<version>/<file>")
            parts = key.split('/')
            if len(parts) >= 3:
                publisher, name, version = parts[0], parts[1], parts[2]
                ext_id = f"{publisher}.{name}"
                # 該当バージョンをVersionsリストから削除
                # まず現在のVersionsを取得
                item = table.get_item(Key={'ExtensionId': ext_id}).get('Item')
                if item:
                    versions = item.get('Versions', [])
                    if version in versions:
                        versions.remove(version)
                        if versions:
                            # まだ他バージョンがある場合は更新
                            table.update_item(
                                Key={'ExtensionId': ext_id},
                                UpdateExpression="SET #vers = :vers",
                                ExpressionAttributeNames={'#vers': 'Versions'},
                                ExpressionAttributeValues={':vers': versions}
                            )
                        else:
                            # 他にバージョンが無ければ項目自体を削除
                            table.delete_item(Key={'ExtensionId': ext_id})
上記Lambdaは、VSIX内のpackage.jsonからメタ情報を取り出し、DynamoDBに格納しています。ZIPの展開と解析を行うため、Lambda関数のタイムアウトやメモリ設定はVSIXサイズに見合った値に調整します。拡張が大量に追加されるケースでは、キュー(SQS)を介して順次処理するなどのスケーリングも検討できますが、通常のサイズ・頻度であれば直接トリガーで問題ありません。

🔹 S3ディレクトリ構造と権限設定

S3ディレクトリ構造は先述の通り、パブリッシャーごとにフォルダを分け、その下に拡張名フォルダ、さらにバージョンフォルダを作ってVSIXファイルを配置します。例えば:
• s3://<BucketName>/ms-python/python/2022.14.0/ms-python.python-2022.14.0.vsix
• s3://<BucketName>/ms-python/python/2023.5.0/ms-python.python-2023.5.0.vsix
• s3://<BucketName>/vscodevim/vim/1.24.1/vscodevim.vim-1.24.1.vsix

といった形になります。この構造によりURLパスが規則的になるため、CloudFront配信でもそのままパスを公開できますし、DynamoDBの拡張ID・バージョンと直接マッピングできます。

IAMポリシーの設定については、各コンポーネントが必要な最低権限を持つようにします。具体的には:
• 検索Lambda用ロール: DynamoDBテーブルへのQuery/Get/Scan権限、およびS3署名URLを発行またはオブジェクトメタデータ確認のためのs3:GetObject権限(バケットリソース指定)。CloudFrontを使う場合、Lambda自体はS3に触れないためS3権限は不要ですが、ここでは署名URL方式も考慮し付与します。
• VSIX配信Lambda用ロール (署名URL発行をする場合のみ): 該当S3バケットのGetObject権限。署名URL生成自体はS3への直接アクセスではありませんが、対象オブジェクトにアクセスできる資格が求められるためです。リダイレクト応答するだけならDynamo権限は不要です。
• メタデータ更新Lambda用ロール: S3バケットからVSIXを取得する権限 (s3:GetObject)、DynamoDBテーブルへのPutItem/UpdateItem/DeleteItem権限。加えて、LambdaがCloudWatch Logsにログ出力するためのログ書き込み権限も各ロールに共通して必要です。また、このLambdaはS3によって起動されるため、Lambda関数リソースポリシーにS3からのinvokeを許可する文が自動付与されます (コンソールでトリガー設定時に付与される)。
• S3バケットポリシー/CloudFront OAI設定: バケット自体には基本的にパブリックアクセスを無効化し、必要に応じてCloudFrontのOAIからのGetObjectを許可するポリシーを設定します。管理者が手動でアップロードする場合はIAMユーザやロール経由でPutObjectできるよう権限を与えます。

以上が、code-serverから拡張機能を検索・インストールできるマーケットプレイス機能をAWSサーバレス上で構築するためのアーキテクチャおよび実装策です。S3とDynamoDBで堅牢かつスケーラブルなメタデータ管理を行い、API Gateway + Lambdaで既存Marketplace互換の検索APIを提供、CloudFrontで効率的にVSIXを配信する構成としました。これにより、オンプレミスや閉域網環境でもcode-server利用者はインターネット経由の公式マーケットプレイスと遜色ない体験で拡張機能を検索・導入できるようになります。参考実装コードも示した通り比較的シンプルなLambdaロジックで実現可能であり、サーバ管理不要な完全マネージドサービスで構成されるためメンテナンスコストも低減できます。ぜひこの構成をベースに最適な環境構築を検討してください。

いいなと思ったら応援しよう!