-
Notifications
You must be signed in to change notification settings - Fork 1
Description
現状の課題
デプロイのたびに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に関連付けられていないため、認証エラーが発生
解決策の提案
- デプロイ時に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 }} fidone
- 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がある場合は、デプロイワークフローでの対応が必要です。
- 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キー管理の長期的解決策
短期的な対応ではなく、長期的な解決策に焦点を当てた設計を提案します。
長期的なアーキテクチャ設計
- 中央集権型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"
}]'
- 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 fidone
実装ロードマップ
フェーズ1: 基盤構築
APIキー管理サービスの基本インフラ構築
データモデルの設計と実装
デプロイイベントリスナーの実装
フェーズ2: 自動化機能
デプロイ時の自動関連付け機能
APIキー管理APIの実装
E2Eテストの強化
フェーズ3: UI開発
管理UI開発
カスタマーポータル開発
使用状況モニタリング機能
フェーズ4: 高度な機能
APIキー自動ローテーション
異常検知と通知
課金連携機能
まとめ
この長期的な解決策により、以下のメリットが得られます:
デプロイの信頼性向上: APIキーの関連付けが自動化され、E2Eテストの失敗が防止される
運用負荷の軽減: 手動でのAPIキー管理が不要になり、運用コストが削減される
スケーラビリティの向上: カスタマー数が増加しても柔軟に対応できる
セキュリティの強化: APIキーの一元管理により、セキュリティポリシーの適用が容易になる
顧客体験の向上: カスタマーポータルにより、顧客自身でAPIキー管理が可能になる
この設計は、現在のBlue/Greenデプロイメント戦略と完全に互換性があり、既存のインフラストラクチャを活用しながら、APIキー管理の課題を解決します。