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

Lambda関数の品質を自動で守る!僕らが実践した「コンプライアンステスト」の全記録

タグ: 🏷 AWS ,Lambda ,Golang ,CI/CD

背景

チーム開発や長期プロジェクトでは、複数のLambda関数がそれぞれ異なる品質基準で実装されてしまう、なんて問題がよく起こりがちです。例えば、こんなケースに心当たりはありませんか?

  • ある関数はエラーハンドリングが完璧、別の関数は最低限
  • ログの出力形式がバラバラで運用時に困る
  • セキュリティ要件が一部の関数でしか守られていない
  • 新しい開発者が既存のパターンを知らずに別の方法で実装してしまう

僕たちのプロジェクトでも、11個ものLambda関数を管理する必要があり、手動でのチェックには限界を感じていました。そこで、コンプライアンステストという仕組みを導入し、品質要件の自動チェックを実現したんです。

この記事では、実際のテストコードを交えながら、なぜこの仕組みが必要だったのか、そしてどのように実装していったのかを詳しくご紹介します。

コンプライアンステストとは?

基本概念

コンプライアンステストとは、コードが決められた品質基準や規則にしっかり従っているかを、自動でチェックしてくれるテストのことです。

AIがコンプライアンステストというので、それにのっとってます。が、言ってしまえば静的解析によりコーディングルールが守られているかをチェックすることです。

機能テスト vs コンプライアンステスト

  • 機能テスト: 「蛇口をひねれば水が出るか」のように、機能が正しく動作するかをテストします。
  • コンプライアンステスト: 「すべての部屋に火災報知器が設置されているか」のように、すべての部品が決められたルールや標準を守っているかを一斉にチェックします。
機能テスト:
入力: {"email": "test@example.com", "password": "test123"}
出力: {"token": "jwt_token", "user": {...}}
→ 機能が正しく動作するかをテスト

コンプライアンステスト:
対象: Lambda関数のソースコード
チェック: 
- 構造化ログを使っているか?
- メトリクス収集を実装しているか?  
- パニック回復処理があるか?
- セキュリティマスキングを適用しているか?
→ 実装の品質と統一性をテスト

なぜ必要なのか?

問題1: 人的ミスによる品質のばらつき

新しい関数を追加する際、実装のルールをうっかり忘れてしまうことがあります。AIにコードを書いてもらう際も、AIが忘れてしまうことが多いです(今の技術だと、全てのコンテキストを渡せないから)。

// 既存のLambda関数(良い例)
func main() {
    log := logger.New("function-name") // ルール通りのログ
    metricsClient := middleware.NewMetricsClient() // ルール通りのメトリクス
    
    // 予期せぬエラーが起きてもプログラムが落ちないようにするおまじない
    defer func() {
        if r := recover(); r != nil {
            log.Error("Panic recovered", r)
        }
    }()
    
    lambda.Start(handler)
}

// 新しい開発者が追加した関数(問題のある例)  
func main() {
    // ログ、メトリクス、エラー回復処理が実装されていない!
    lambda.Start(handler)
}

この状態では、新しい関数だけ品質が低く、監視や運用で問題が発生します。

問題2: 要件の変更・追加への対応漏れ

後から「すべての関数にセキュリティ機能を追加しよう」となっても、手作業では必ず漏れが発生します。

実際のコンプライアンステスト実装

ここからは、Go言語のテスト機能を使って、ソースコード自体をチェックするテストの実装方法を見ていきます。

1. テスト対象の明確化

まず、品質をチェックしたいLambda関数の一覧を定義します。

// pkg/lambda/consistency_test.go
func TestLambdaFunctionConsistency(t *testing.T) {
    // これからチェックする関数の一覧
    lambdaFunctions := []string{
        "../../cmd/register/main.go",
        "../../cmd/login/main.go", 
        // ... 合計11個の関数
    }
    // ... この後、各関数をループでチェックしていく
}

2. 構造化ログの統一性チェック

要件: すべてのLambda関数で、我々が決めたlogger.Newというログの仕組みを使うこと。

// ...

// 実際のチェック処理
func CheckStructuredLogging(filePath string) bool {
    // Goのソースコードをテキストとして読み込む
    content, err := os.ReadFile(filePath)
    if err != nil {
        return false
    }
    
    codeStr := string(content)
    
    // 【正規表現】ソースコード内に 'logger.New(' という文字列があるかチェック
    loggerNewPattern := `logger\.New\s*\(`
    matched, _ := regexp.MatchString(loggerNewPattern, codeStr)
    if matched {
        return true
    }
    
    // 【正規表現】または、推奨している共通部品 'lambdaCommon.NewCommonHandler' が使われているかチェック
    commonHandlerPattern := `lambdaCommon\.NewCommonHandler`
    matched, _ = regexp.MatchString(commonHandlerPattern, codeStr)
    return matched
}

3. メトリクス収集の統一性チェック

要件: すべての関数でパフォーマンス指標(メトリクス)を収集していること。

// ...
func CheckMetricsCollection(filePath string) bool {
    content, err := os.ReadFile(filePath)
    if err != nil {
        return false
    }
    codeStr := string(content)
    
    // パフォーマンス測定に関連するコードが存在するかチェック
    patterns := []string{
        `middleware\.NewMetricsClient\s*\(`, // メトリクス収集部品
        `time\.Now\s*\(\)`,               // 開始時間の記録
        `time\.Since\s*\(`,              // 実行時間の計算
        `lambdaCommon\.NewCommonHandler`, // 共通部品
    }
    
    for _, pattern := range patterns {
        if matched, _ := regexp.MatchString(pattern, codeStr); matched {
            return true
        }
    }
    return false
}

4. パニック回復処理のチェック

要件: 予期しないエラー(パニック)が発生しても、サーバーが完全に停止しないように回復処理を入れること。

// ...
func CheckPanicRecovery(filePath string) bool {
    content, err := os.ReadFile(filePath)
    if err != nil {
        return false
    }
    codeStr := string(content)
    
    // パニック回復のお決まりパターンが存在するかチェック
    patterns := []string{
        `defer\s+func\s*\(\s*\)\s*{[\s\S]*recover\s*\(\s*\)`,  // deferとrecoverの組み合わせ
        `recover\s*\(\s*\)`,                                    // recover()の呼び出し
        `lambdaCommon\.NewCommonHandler`,                       // 共通部品
    }
    
    for _, pattern := range patterns {
        if matched, _ := regexp.MatchString(pattern, codeStr); matched {
            return true
        }
    }
    return false
}

5. セキュリティログの統一性チェック

要件: パスワードなどの機密情報をログに出力しないように、安全な仕組みを使っていること。

// ...
func CheckSecureLogging(filePath string) bool {
    content, err := os.ReadFile(filePath)
    if err != nil {
        return false
    }
    codeStr := string(content)
    
    // 危険なログ出力パターンがないかチェック
    dangerousPatterns := []string{
        `log\..*password`,     // 例: log.Printf("password: %s", password)
        `log\..*Password`,
        `log\..*token.*request`, // リクエスト全体をログ出力している
        `fmt\.Printf.*password`,
    }
    
    for _, pattern := range dangerousPatterns {
        if matched, _ := regexp.MatchString(pattern, codeStr); matched {
            return false // 危険なパターンが見つかったら即NG
        }
    }
    
    // 安全なログ出力方法(マスキング機能付きなど)の存在をチェック
    safePatterns := []string{
        `logger\.New\s*\(`,      // 構造化ロガー
        `MaskSensitiveData\s*\(`, // マスキング関数
        `lambdaCommon\.NewCommonHandler`, // 共通部品
    }
    
    for _, pattern := range safePatterns {
        if matched, _ := regexp.MatchString(pattern, codeStr); matched {
            return true
        }
    }
    
    return false
}

コンプライアンステストの結果(エラーが出たパターン)

ルールに反していると、以下のようにエラーが出力されます。

=== RUN   TestLambdaFunctionCompliance/post-confirmation
    consistency_test.go:370: Function ../../cmd/post-confirmation/main.go does not meet all compliance requirements:
          ✅ Structured Logging: implemented
          ✅ Metrics Collection: implemented
          ✅ Panic Recovery: implemented
          ✅ CORS Middleware: implemented
          ✅ Request Logging: implemented
          ✅ Security Logging: compliant
          ❌ Performance Metrics: missing
          ❌ Error Handling: missing

TDD(Test-Driven Development)によるアプローチ

Red-Green-Refactorサイクル

TDDとは? テスト駆動開発(TDD)は、いわゆるt.wadaのアレです。 もう少し具体的に言うと、プログラム本体を書く前に「失敗するテスト」を先に書く開発手法です。

  1. Red: まず、失敗するテストを書きます。(テストが赤信号になる)
  2. Green: テストが通る最小限のコードを書きます。(テストが青信号になる)
  3. Refactor: コードを綺麗に整理します。(信号は青のまま) このサイクルを繰り返すことで、品質の高いコードを効率的に作れます。

私たちはこのTDDの手法で、コンプライアンステストを導入しました。

Phase 1: Red(失敗するテストを書く)

最初に書いたコンプライアンステストは、既存のコードがルールを守っていないため、当然すべて失敗します。

Phase 2: Green(テストを通すための実装)

各Lambda関数を一つずつ修正するのは大変なので、「共通ハンドラーパターン」 という部品を作って、それで既存の関数を包み込むようにしました。この共通部品に品質要件(ログ、メトリクス、エラー回復など)をすべて実装することで、すべての関数がルールを守るようになります。

Phase 3: Refactor(コードの改善)

すべてのテストが通るようになった後、今度は「共通ハンドラー」自体のコードをより効率的で読みやすいように整理しました。

最終的なテスト結果

すべてのテストがパスし、11個すべてのLambda関数が同じ品質基準を満たしていることが自動的に保証されるようになりました。

実装された品質要件

この仕組みにより、すべてのLambda関数で以下の品質が自動的に保証されるようになりました。

  • 構造化ログ: ログの形式が統一され、後からの分析が容易に。
  • メトリクス収集: パフォーマンス指標が自動で収集され、問題の早期発見が可能に。
  • パニック回復: 予期せぬエラーでもサーバーが停止せず、ユーザーへの影響を最小限に。
  • セキュリティマスキング: パスワードなどの機密情報がログに残らないように。
  • 統一されたエラーレスポンス: エラー発生時もユーザーに分かりやすいメッセージを返せるように。

CI/CDパイプラインでの活用

CI/CDとは? Continuous Integration/Continuous Deploymentの略。開発者がコードを変更するたびに、テストやデプロイを自動的に実行してくれる仕組みです。GitHub Actionsなどが有名です。

このコンプライアンステストをCI/CDに組み込むことで、新しいコードが追加・変更されるたびに、品質基準が守られているかを自動でチェックできます。もしルール違反があれば、デプロイが自動的にストップするため、品質の低いコードが本番環境に出てしまうのを防げます。

それでも課題はある

追加したLambda関数をこのpkg/lambda/consistency_test.goに組み込むのを忘れることです。

実際、関数を3つ追加したのですが、このコンプライアンステストに組み込むのを忘れていました。忘れないで組み込む仕組みを考えるのは今後の課題です。

学んだ教訓とベストプラクティス

  1. 段階的導入の重要性: 一度にすべてを完璧にするのではなく、まずはログの統一から、次にメトリクス、というように段階的に導入するのが成功の鍵です。
  2. 共通ハンドラーパターンの威力: 個別の関数を修正するのではなく、共通部品を作ることで、開発効率と保守性が劇的に向上します。
  3. 新しい開発者へのオンボーディング: 新しくチームに参加した人がすぐに品質基準を理解し、守れるように、テンプレートを用意すると効果的です。

なぜこの経験が重要なのか

コンプライアンステストの導入により、私たちは「気をつける」という曖昧な方法から脱却し、品質を仕組みで自動的に保証できるようになりました。

  • 人的ミスの排除: 手動チェックでは見逃しがちなミスを防ぎます。
  • 開発効率の向上: 開発者は品質の心配をせず、ビジネスロジックの実装に集中できます。
  • 運用品質の向上: ログやメトリクスが統一され、問題発生時の原因特定が迅速になります。

これから開発を始める方へ

  • 品質の担保は、個人の頑張りだけに頼らず、仕組みで解決しましょう。
  • テストは「機能が動くか」だけでなく、「ルールが守られているか」もチェックできます。
  • 一度作った仕組みは、将来にわたってチーム全体の生産性を大きく向上させる資産になります。