Skip to content

APIキー管理 #72

@laysakura

Description

@laysakura

現状の課題

デプロイのたびにAPIキーのステージが外れる

解消方法

真面目にAPIキー管理をすることをClineに勧められている


問題分析: APIキーと「関連付けられたAPI」の問題
調査した結果、問題の原因と解決策が見えてきました。

問題の概要
APIをデプロイすると、APIキーに紐づいた「関連付けられた API」が消えてしまい、E2Eテストが失敗しています。

原因分析
Blue/Greenデプロイメントの仕組み:

現在のシステムでは、Blue/Greenデプロイメント戦略を採用しています
新しいAPIをデプロイする際に、新しいスタック(Blue/Green)を作成し、古いスタックは一定時間後に削除される仕組み
APIキーとUsage Planの管理:

APIキーとUsage Planはshared-api-resources.yamlで定義され、環境ごとに共有リソースとして管理
APIキーはカスタマーごとに個別に発行され、特定のUsage Planに関連付けられる
問題の発生メカニズム:

新しいAPIがデプロイされると、カスタムドメインのマッピングは新しいAPIに更新される
しかし、APIキーとUsage Planの関連付けが新しいAPIに自動的に引き継がれていない
具体的には、UsagePlanStageAssociationが新しいAPIに対して作成されていない
E2Eテストの失敗原因:

E2Eテストワークフロー(e2e-test-api.yml)では、APIキーを取得して使用している
しかし、そのAPIキーが新しいAPIに関連付けられていないため、認証エラーが発生
解決策の提案

  1. デプロイ時にAPIキーの関連付けを更新
    deploy-api.ymlワークフローに、新しいAPIとUsage Planの関連付けを行うステップを追加します:

新しいAPIとUsage Planの関連付け

  • name: Associate Usage Plans with new API
    run: |

    環境に応じたUsage Plan IDを取得

    BASIC_PLAN_ID=$(aws cloudformation describe-stacks
    --stack-name rapidpen-cloud-shared-api-resources-${{ github.event.inputs.environment }}
    --query "Stacks[0].Outputs[?OutputKey=='BasicUsagePlanId'].OutputValue"
    --output text)

    STANDARD_PLAN_ID=$(aws cloudformation describe-stacks
    --stack-name rapidpen-cloud-shared-api-resources-${{ github.event.inputs.environment }}
    --query "Stacks[0].Outputs[?OutputKey=='StandardUsagePlanId'].OutputValue"
    --output text)

    ENTERPRISE_PLAN_ID=$(aws cloudformation describe-stacks
    --stack-name rapidpen-cloud-shared-api-resources-${{ github.event.inputs.environment }}
    --query "Stacks[0].Outputs[?OutputKey=='EnterpriseUsagePlanId'].OutputValue"
    --output text)

    各Usage PlanにAPIステージを関連付け

    for PLAN_ID in "$BASIC_PLAN_ID" "$STANDARD_PLAN_ID" "$ENTERPRISE_PLAN_ID"; do
    echo "Associating Usage Plan $PLAN_ID with new API ${{ steps.new-api.outputs.id }}"

    # 既存の関連付けを確認
    EXISTING_STAGES=$(aws apigateway get-usage-plan \
      --usage-plan-id $PLAN_ID \
      --query "apiStages" \
      --output json)
    
    # 既存の関連付けがある場合は更新、なければ作成
    if [[ $EXISTING_STAGES == *"${{ steps.new-api.outputs.id }}"* ]]; then
      echo "API already associated with Usage Plan, updating..."
      aws apigateway update-usage-plan \
        --usage-plan-id $PLAN_ID \
        --patch-operations op=replace,path=/apiStages/0/apiId,value=${{ steps.new-api.outputs.id }} op=replace,path=/apiStages/0/stage,value=${{ github.event.inputs.environment }}
    else
      echo "Creating new association..."
      aws apigateway update-usage-plan \
        --usage-plan-id $PLAN_ID \
        --patch-operations op=add,path=/apiStages,value=apiId=${{ steps.new-api.outputs.id }},stage=${{ github.event.inputs.environment }}
    fi
    

    done

  1. SAMテンプレートの修正(オプション)
    wish/wish-log-analysis-api/template.yamlのデプロイ時に、APIキーとUsage Planの関連付けを自動的に行うように修正することも検討できます:

UsagePlanとAPIステージの関連付け

UsagePlanStageAssociation:
Type: AWS::ApiGateway::UsagePlanStage
Condition: HasUsagePlan
DependsOn: ApiGatewayApiStage
Properties:
UsagePlanId: !Ref UsagePlanId
ApiId: !Ref ApiGatewayApi
Stage: !Ref Environment
ただし、この方法は単一のUsage Planに対してのみ機能します。複数のUsage Planがある場合は、デプロイワークフローでの対応が必要です。

  1. APIキー管理の自動化
    長期的な解決策として、APIキー管理を自動化するシステムの構築を検討できます:

APIキー管理Lambda: デプロイ時に自動的にAPIキーとUsage Planの関連付けを更新
カスタマー管理システム: カスタマー情報とAPIキーを一元管理
自動テスト: デプロイ後にAPIキーの関連付けが正しく行われたことを確認するテスト
実装計画
まず、deploy-api.ymlワークフローに上記のステップを追加して、問題の即時解決を図る
E2Eテストが成功することを確認
長期的には、APIキー管理の自動化システムを構築することを検討
図解
Yes

No

デプロイ開始

新しいAPIスタックをデプロイ

カスタムドメインマッピングを更新

新しいAPIとUsage Planを関連付け

古いAPIスタックの削除をスケジュール

デプロイ完了

E2Eテスト

APIキーが新しいAPIに関連付けられている?

テスト成功

テスト失敗

この解決策を実装することで、APIデプロイ後もAPIキーと「関連付けられたAPI」の関係が維持され、E2Eテストが成功するようになります。

短期はやらないで良い。長期策をお願い

APIキー管理の長期的解決策
短期的な対応ではなく、長期的な解決策に焦点を当てた設計を提案します。

長期的なアーキテクチャ設計

  1. 中央集権型APIキー管理システム
    API Key Management Service

DynamoDB: API Keys

DynamoDB: Usage Plans

DynamoDB: Customer Info

API Gateway

Blue/Green Deployment

E2E Tests

Admin UI

Customer Portal

主要コンポーネント
API Key Management Service

独立したマイクロサービスとして実装
APIキー、Usage Plan、カスタマー情報の一元管理
デプロイイベントのリスナーとして機能
データストア

APIキーテーブル: キーID、値、関連APIリスト、有効期限など
Usage Planテーブル: プランID、制限値、関連APIリスト
カスタマーテーブル: カスタマー情報、契約プラン、APIキー参照
イベント駆動型アーキテクチャ

デプロイイベントをEventBridgeで発行
APIキー管理サービスがイベントをサブスクライブ
デプロイ完了時に自動的にAPIキー関連付けを更新
2. APIキー管理サービスの実装

api-key-management-service.yaml

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: API Key Management Service

Resources:

APIキー管理テーブル

ApiKeysTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: api-keys
BillingMode: PAY_PER_REQUEST
AttributeDefinitions:
- AttributeName: id
AttributeType: S
KeySchema:
- AttributeName: id
KeyType: HASH

Usage Plan管理テーブル

UsagePlansTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: usage-plans
BillingMode: PAY_PER_REQUEST
AttributeDefinitions:
- AttributeName: id
AttributeType: S
KeySchema:
- AttributeName: id
KeyType: HASH

カスタマー管理テーブル

CustomersTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: customers
BillingMode: PAY_PER_REQUEST
AttributeDefinitions:
- AttributeName: id
AttributeType: S
KeySchema:
- AttributeName: id
KeyType: HASH

デプロイイベントリスナー

DeploymentEventFunction:
Type: AWS::Serverless::Function
Properties:
Handler: src/handlers/deployment-event.handler
Runtime: nodejs18.x
Policies:
- DynamoDBCrudPolicy:
TableName: !Ref ApiKeysTable
- DynamoDBCrudPolicy:
TableName: !Ref UsagePlansTable
- Statement:
- Effect: Allow
Action:
- apigateway:GET
- apigateway:PUT
- apigateway:POST
- apigateway:DELETE
Resource: '*'
Events:
ApiDeploymentEvent:
Type: EventBridgeRule
Properties:
Pattern:
source:
- "rapidpen-cloud.deployment"
detail-type:
- "API Deployment Completed"

APIキー管理API

ApiKeyManagementApi:
Type: AWS::Serverless::Api
Properties:
StageName: prod
Auth:
DefaultAuthorizer: AdminAuthorizer
Authorizers:
AdminAuthorizer:
FunctionArn: !GetAtt AdminAuthorizerFunction.Arn

APIキー管理Lambda関数

ApiKeyManagementFunction:
Type: AWS::Serverless::Function
Properties:
Handler: src/handlers/api-key-management.handler
Runtime: nodejs18.x
Policies:
- DynamoDBCrudPolicy:
TableName: !Ref ApiKeysTable
- DynamoDBCrudPolicy:
TableName: !Ref UsagePlansTable
- DynamoDBCrudPolicy:
TableName: !Ref CustomersTable
- Statement:
- Effect: Allow
Action:
- apigateway:GET
- apigateway:PUT
- apigateway:POST
- apigateway:DELETE
Resource: '*'
Events:
CreateApiKey:
Type: Api
Properties:
RestApiId: !Ref ApiKeyManagementApi
Path: /api-keys
Method: post
GetApiKeys:
Type: Api
Properties:
RestApiId: !Ref ApiKeyManagementApi
Path: /api-keys
Method: get
UpdateApiKey:
Type: Api
Properties:
RestApiId: !Ref ApiKeyManagementApi
Path: /api-keys/{id}
Method: put
DeleteApiKey:
Type: Api
Properties:
RestApiId: !Ref ApiKeyManagementApi
Path: /api-keys/{id}
Method: delete
3. デプロイプロセスの改善
DynamoDB
API Key Management Service
EventBridge
API Gateway
CI/CD Pipeline
DynamoDB
API Key Management Service
EventBridge
API Gateway
CI/CD Pipeline
デプロイ新しいAPI (Blue/Green)
カスタムドメインマッピング更新
デプロイ完了イベント発行
デプロイイベント通知
現在のAPIキー情報取得
全APIキーを新APIに関連付け
APIキー関連付け情報更新
E2Eテスト実行
デプロイワークフローの修正
イベント発行機能の追加

deploy-api.ymlに追加

  • name: Publish deployment event
    run: |

    デプロイ完了イベントをEventBridgeに発行

    aws events put-events --entries '[{
    "Source": "rapidpen-cloud.deployment",
    "DetailType": "API Deployment Completed",
    "Detail": "{"environment": "${{ github.event.inputs.environment }}", "apiId": "${{ steps.new-api.outputs.id }}", "stage": "${{ github.event.inputs.environment }}", "stackName": "${{ steps.deployment-state.outputs.target_stack }}"}",
    "EventBusName": "default"
    }]'
  1. APIキー管理サービスの主要機能
    デプロイイベントハンドラー
    // src/handlers/deployment-event.js
    const AWS = require('aws-sdk');
    const dynamodb = new AWS.DynamoDB.DocumentClient();
    const apigateway = new AWS.APIGateway();

exports.handler = async (event) => {
console.log('Received event:', JSON.stringify(event, null, 2));

// イベントからデプロイ情報を取得
const { environment, apiId, stage, stackName } = event.detail;

try {
// 1. 環境に関連するUsage Planを取得
const usagePlans = await getUsagePlansForEnvironment(environment);

// 2. 各Usage Planに新しいAPIを関連付け
for (const plan of usagePlans) {
  await associateApiWithUsagePlan(plan.id, apiId, stage);
}

// 3. APIキー情報を更新
await updateApiKeyAssociations(apiId, environment);

console.log(`Successfully updated API associations for ${apiId} in ${environment}`);
return { statusCode: 200, body: 'Success' };

} catch (error) {
console.error('Error updating API associations:', error);
throw error;
}
};

// 環境に関連するUsage Planを取得
async function getUsagePlansForEnvironment(environment) {
const params = {
TableName: 'usage-plans',
FilterExpression: 'environment = :env',
ExpressionAttributeValues: { ':env': environment }
};

const result = await dynamodb.scan(params).promise();
return result.Items;
}

// Usage PlanにAPIを関連付け
async function associateApiWithUsagePlan(usagePlanId, apiId, stage) {
// 既存の関連付けを確認
const usagePlan = await apigateway.getUsagePlan({ usagePlanId }).promise();

const existingStage = usagePlan.apiStages.find(s => s.apiId === apiId && s.stage === stage);

if (existingStage) {
// 既存の関連付けがある場合は更新
await apigateway.updateUsagePlan({
usagePlanId,
patchOperations: [
{
op: 'replace',
path: /apiStages/${apiId}:${stage}/throttle/rateLimit,
value: usagePlan.throttle.rateLimit.toString()
},
{
op: 'replace',
path: /apiStages/${apiId}:${stage}/throttle/burstLimit,
value: usagePlan.throttle.burstLimit.toString()
}
]
}).promise();
} else {
// 新しい関連付けを作成
await apigateway.updateUsagePlan({
usagePlanId,
patchOperations: [
{
op: 'add',
path: '/apiStages',
value: ${apiId}:${stage}
}
]
}).promise();
}
}

// APIキー情報を更新
async function updateApiKeyAssociations(apiId, environment) {
// 環境に関連するAPIキーを取得
const params = {
TableName: 'api-keys',
FilterExpression: 'environment = :env',
ExpressionAttributeValues: { ':env': environment }
};

const result = await dynamodb.scan(params).promise();

// 各APIキーの関連APIリストを更新
for (const apiKey of result.Items) {
// 既存のAPIリストから古いAPIを削除し、新しいAPIを追加
const updatedApis = apiKey.associatedApis.filter(api => !api.includes(-${environment}-));
updatedApis.push(apiId);

await dynamodb.update({
  TableName: 'api-keys',
  Key: { id: apiKey.id },
  UpdateExpression: 'SET associatedApis = :apis',
  ExpressionAttributeValues: { ':apis': updatedApis }
}).promise();

}
}
5. 管理UIとカスタマーポータル
管理UI機能

APIキーの作成・管理
Usage Planの設定
カスタマー情報の管理
使用状況のモニタリング
カスタマーポータル機能

APIキーの表示・再発行
使用状況の確認
プランのアップグレード/ダウングレード要求
ドキュメントとサンプルコードへのアクセス
6. 自動テストの強化
E2Eテストワークフローに、APIキー関連付けの検証ステップを追加:

e2e-test-api.ymlに追加

  • name: Verify API key association
    id: verify-api-key
    run: |

    APIキーの関連付けを確認

    API_KEY_ID=$(aws apigateway get-api-keys
    --name-query "$API_KEY_NAME"
    --include-values
    --query "items[0].id"
    --output text)

    APIキーに関連付けられたUsage Planを取得

    USAGE_PLANS=$(aws apigateway get-usage-plans
    --key-id "$API_KEY_ID"
    --query "items[*].id"
    --output json)

    各Usage Planに新しいAPIが関連付けられているか確認

    for PLAN_ID in $(echo $USAGE_PLANS | jq -r '.[]'); do
    PLAN_DETAILS=$(aws apigateway get-usage-plan
    --usage-plan-id "$PLAN_ID"
    --query "apiStages"
    --output json)

    if [[ $PLAN_DETAILS == *"$API_ID"* ]]; then
      echo "API $API_ID is correctly associated with Usage Plan $PLAN_ID"
    else
      echo "ERROR: API $API_ID is NOT associated with Usage Plan $PLAN_ID"
      exit 1
    fi
    

    done
    実装ロードマップ
    フェーズ1: 基盤構築

APIキー管理サービスの基本インフラ構築
データモデルの設計と実装
デプロイイベントリスナーの実装
フェーズ2: 自動化機能

デプロイ時の自動関連付け機能
APIキー管理APIの実装
E2Eテストの強化
フェーズ3: UI開発

管理UI開発
カスタマーポータル開発
使用状況モニタリング機能
フェーズ4: 高度な機能

APIキー自動ローテーション
異常検知と通知
課金連携機能
まとめ
この長期的な解決策により、以下のメリットが得られます:

デプロイの信頼性向上: APIキーの関連付けが自動化され、E2Eテストの失敗が防止される
運用負荷の軽減: 手動でのAPIキー管理が不要になり、運用コストが削減される
スケーラビリティの向上: カスタマー数が増加しても柔軟に対応できる
セキュリティの強化: APIキーの一元管理により、セキュリティポリシーの適用が容易になる
顧客体験の向上: カスタマーポータルにより、顧客自身でAPIキー管理が可能になる
この設計は、現在のBlue/Greenデプロイメント戦略と完全に互換性があり、既存のインフラストラクチャを活用しながら、APIキー管理の課題を解決します。

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions