メインコンテンツにスキップ

Go製AWS Lambdaのパフォーマンスとセキュリティを同時に改善した3つの実践テクニック

はじめに

サーバーレスアプリケーション、特にAWS Lambdaを利用する上で、コールドスタートによるパフォーマンスの低下は多くの開発者が直面する課題です。また、外部サービスと連携する際には、セキュリティ、特にCORS(Cross-Origin Resource Sharing)の設定にも細心の注意が求められます。

この記事では、AWS Cognitoと連携するGo言語製の認証Lambdaを例に、パフォーマンス問題の解決CORSセキュリティの強化を同時に実現した、以下の3つの改善プロセスを詳しく解説します。

  1. Cognitoクライアントのキャッシュによる高速化
  2. タイムアウト制御による安定化
  3. 厳格なCORSミドルウェアによるセキュリティ強化

改善前の課題

🚨 問題1:毎リクエストでのCognitoクライアント生成

変更前のコード(cmd/login/main.go):

func businessHandler(ctx context.Context, request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
    
    // ❌ 毎回新しいクライアントを作成
    cognitoClient, err := cognito.NewClient()
    if err != nil {
        return utils.CreateErrorResponse(500, "Failed to initialize Cognito client"), nil
    }
    
    // ... ビジネスロジック
}

問題点:

  • Lambda関数が呼び出されるたびにcognito.NewClient()が実行されていました。
  • これにより、ホットスタート時(コンテナが再利用される時)でも、本来は不要な初期化処理が発生していました。
  • 特に、内部でAWS Parameter Storeからシークレットを取得する処理が毎回実行され、レスポンス時間の増加とコスト上昇の要因となっていました。

🚨 問題2:タイムアウト制御の不備

// ❌ 外部サービスへのリクエストにタイムアウト制御がなかった
result, err := cognitoClient.SignUp(signUpInput)

問題点:

  • AWS Cognitoへのリクエストにタイムアウトが設定されていませんでした。
  • ネットワークの一時的な問題などでCognitoからの応答がない場合、Lambda関数が応答を待ち続けてしまい、タイムアウトするまでハングする可能性がありました。

🚨 問題3:CORSセキュリティの脆弱性

変更前のCORSロジック(pkg/middleware/cors.go):

func (c *CORSMiddleware) selectOrigin(requestOrigin string) string {
    // ...(中略)...

    // ❌ 許可されていないオリジンからのリクエストでも、
    //    許可リストの最初のオリジンを返してしまっていた
    return c.allowedOrigins[0]
}

問題点:

  • 許可リストにないオリジンからのリクエストでも、誤って許可リストの最初のオリジンをAccess-Control-Allow-Originヘッダーに設定して返していました。
  • これにより、意図しないオリジンからのAPI利用が可能になり、セキュリティ上、脆弱な状態でした。

解決策の実装

🔧 解決策1:Thread-SafeなCognitoクライアントキャッシュ

Lambdaの実行環境が再利用されることを活かし、一度生成したCognitoクライアントをグローバル変数にキャッシュします。

新規作成:pkg/cognito/client_cache.go

package cognito

import (
    "os"
    "sync"
)

var (
    cachedClient *Client
    cacheOnce    sync.Once // 複数のgoroutineから呼ばれても一度しか実行しないことを保証するオブジェクト
    cacheError   error
)

// GetCachedClient はCognitoクライアントのキャッシュされたインスタンスを返す
func GetCachedClient() (*Client, error) {
    // cacheOnce.Doの中の処理は、この関数が何回呼ばれても、
    // 並行していくつのgoroutineから呼ばれても、全体で一度しか実行されない。
    cacheOnce.Do(func() {
        // この初期化処理が実行されるのは、プロセスで本当に最初の1回だけ
        cachedClient, cacheError = NewClient()
    })
    
    return cachedClient, cacheError
}

Lambda関数での使用方法(cmd/login/main.go):

func businessHandler(ctx context.Context, request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
    
    // ✅ キャッシュされたクライアントを取得
    cognitoClient, err := cognito.GetCachedClient()
    if err != nil {
        // ... エラー処理
        return utils.CreateErrorResponse(500, "Service temporarily unavailable"), nil
    }
    
    // ... ビジネスロジック
}

このsync.Onceを使ったパターンにより、Lambdaのホットスタート時にはNewClient()が再実行されなくなり、パフォーマンスが劇的に向上します。

🔧 解決策2:タイムアウト制御の実装

Goのcontextパッケージを使い、外部サービス呼び出しにタイムアウトを設定します。

改善されたCognitoクライアント(pkg/cognito/client.go):

import "context"
import "time"

// WithContextを使用してタイムアウト制御を追加
func (c *Client) LoginWithContext(ctx context.Context, email, password string) (*LoginResult, error) {
    // 親のコンテキスト(ctx)を使い、15秒でタイムアウトする新しいコンテキストを作成
    timeoutCtx, cancel := context.WithTimeout(ctx, 15*time.Second)
    defer cancel() // 処理終了時にリソースを解放
    
    // ✅ タイムアウト制御付きでCognito APIを呼び出し
    result, err := c.client.InitiateAuthWithContext(timeoutCtx, &cognitoidentityprovider.InitiateAuthInput{
        // ...
    })
    
    if err != nil {
        // エラーがタイムアウトによるものか判定
        if timeoutCtx.Err() == context.DeadlineExceeded {
            // ... タイムアウト専用のエラーを返す
        }
        return nil, c.handleCognitoError(err, "login")
    }
    
    // ...
}

🔧 解決策3:セキュアなCORS実装

許可されていないオリジンからのリクエストを明示的に拒否するよう、ミドルウェアを修正します。

改善後のCORSロジック(pkg/middleware/cors.go):

// selectOrigin は厳密なオリジン検証を行う
func (c *CORSMiddleware) selectOrigin(requestOrigin string) string {
    // ...(ワイルドカード等のチェック)...

    // リクエストヘッダーにOriginがない場合は何も返さない
    if requestOrigin == "" {
        return ""
    }

    // ✅ 許可リストと完全に一致するかをチェック
    for _, o := range c.allowedOrigins {
        if o == requestOrigin {
            return requestOrigin // 一致した場合のみ、そのオリジンを返す
        }
    }

    // ✅ どの許可リストにも一致しなかった場合は、空文字列を返す
    return ""
}

この修正により、許可リストにないオリジンからのリクエストに対してはAccess-Control-Allow-Originヘッダーがレスポンスに含まれなくなり、ブラウザが正しくリクエストをブロックするようになります。

🔧 解決策4:Test-Driven Development(TDD)の採用

これらの変更が正しく機能することを保証するため、単体テストを記述しました。

CORSセキュリティテストの例(抜粋):

func TestCORSMiddleware_SecurityValidation(t *testing.T) {
    corsMiddleware := NewCORSMiddleware(WithAllowedOrigin("https://trusted-app.com"))
    
    testCases := []struct {
        name         string
        origin       string // テストするリクエストのオリジン
        expectStatus int    // 期待するHTTPステータスコード
        expectOrigin string // 期待するAccess-Control-Allow-Originヘッダーの値
    }{
        {"正当なオリジン", "https://trusted-app.com", 200, "https://trusted-app.com"},
        {"サブドメイン攻撃", "https://malicious.trusted-app.com", 403, ""},
        {"プロトコル変更攻撃", "http://trusted-app.com", 403, ""},
        {"不正なオリジン", "https://evil.com", 403, ""},
    }
    
    // ... テスト実行ロジック ...
}

改善結果

📈 パフォーマンス改善

ベンチマーク結果:

  • 改善前: 12.5ms/op
  • 改善後: 0.15ms/op
  • 改善率: 98.8%

ホットスタート時の不要なクライアント初期化をなくすことで、処理時間が著しく短縮されました。

🔒 セキュリティ強化

厳格なオリジンチェックを実装したことで、意図しないドメインからのAPIリクエストを確実にブロックできるようになりました。

まとめ

今回のリファクタリングから得られた教訓は以下の通りです。

  1. Lambdaでは初期化コストを意識する: handlerの外で初期化し、グローバル変数にキャッシュするパターンは非常に有効です。
  2. 外部通信には必ずタイムアウトを: context.WithTimeoutを使い、システムの安定性を高めます。
  3. CORSはホワイトリスト方式で厳格に: デフォルトで拒否し、許可するものだけを明示的に通します。

パフォーマンスとセキュリティはトレードオフの関係にあると思われがちですが、適切な設計と実装によって、両方を同時に向上させることが可能です。