CodePipeline LambdaからPHPUnitを実行する

CodePipelineによるデプロイの流れでPHPUnitを走らせる際には、CodeBuildの段階でテストを実行するのがセオリーなのだと思います。

しかし諸般の事情でそういうわけにもいかず、デプロイ後にデプロイ先の環境でPHPUnitを走らせたいようなケースがあったりなかったりもすると思うんです。

どうにかしてみましょう。

具体的には、
「CodePipelineのLambda実行ステージでPHPUnitを実行」
 ↓
「PHPunitの実行結果に応じて、Pipelineの処理を進行/停止させる」
というお話が今回の記事となります。

PHP コード内で PHPUnit を呼び出す(実行する)という記事を以前書いたのですが、伏線回収というわけですね。

1.Lambda関数の作成

CodePipelineに設置したLambda実行ステージにて呼び出す関数を作成します。

Lambda実行ステージの設置と、Lambda関数作成自体の手順は割愛。

テスト環境と本番環境で接続先のURLを切り替えるため、CodePipelineのステージで、UserParameterとして「staging」や「production」などの文字を指定するものとします。

import boto3
import urllib.request


def lambda_handler(event, context):
    codepipeline = boto3.client('codepipeline')
    try:
        # Get CodePipeline user param.
        environment = event['CodePipeline.job']['data']['actionConfiguration']['configuration']['UserParameters']
        print('environment:', environment)
        
        # Set url
        if environment == 'staging':
            url = 'テスト環境の接続先URL'
        elif environment == 'production':
            url = '本番環境の接続先URL'
        print('url:', url)

        # Run PHPUnit.
        response = urllib.request.urlopen(url).read().decode("utf-8")
        if 'OK' in response:
            # Return Success to CodePipeline.
            codepipeline = boto3.client('codepipeline')
            codepipeline.put_job_success_result(
                    jobId = event['CodePipeline.job']['id'],
                    executionDetails={
                        'summary': response,
                    }
                )
        else:
            # Return Failure to CodePipeline.
            codepipeline.put_job_failure_result(
                    jobId = event['CodePipeline.job']['id'],
                    failureDetails={
                        'type': 'JobFailed',
                        'message': response
                    }
                )
    except Exception as e:
        print(e)
        # Return Failure to CodePipeline.
        codepipeline.put_job_failure_result(
                jobId = event['CodePipeline.job']['id'],
                failureDetails={
                    'type': 'JobFailed',
                    'message': str(e)
                }
            )
        
    return

デフォルトのLog書き込み権限に加え、下記の権限を与えます。

{
"Effect": "Allow",
"Action": [
"codepipeline:PutJobFailureResult",
"codepipeline:PutJobSuccessResult"
],
"Resource": "*"
}

この関数では、
response = urllib.request.urlopen(url).read().decode("utf-8")
の箇所でPHPUnitの実行スクリプトにアクセスし、実行結果を取得しています。

その後、PHPUnitの実行結果に応じて、Pipelineの処理を進行/停止させています。
▶正常だった場合には、Pipelineに「成功」を返して処理を進めます。
▶問題があった場合には、Pipelineに「失敗」を返して進行を止めます。

2. Lambdaから呼び出すPHPUnit実行スクリプトを用意

Lambda関数へのレスポンスは、
Time: 231 ms, Memory: 14.00MB OK (1 test, 1 assertion)
などの最後の結果部分のみです。
(PHP側もLambda側も、雑に「OK」という文字の有無で成否を判定する手抜き仕様ですがご愛嬌)

実行結果の全文ログをS3に保存するため、実行関数はshell_exec()としました。

<?php

$path_to_phpunit = '/path/to/phpunit';
$path_to_tests = '/path/to/tests';

chdir($path_to_tests);

$result = shell_exec("$path_to_phpunit $path_to_tests 2>&1");
// 適宜必要な出力部分を切り出し
$sub1 = mb_substr($result, 0, mb_strpos($result, 'phpunit.xml') + 13);
$sub2 = $sub1 . mb_substr($result, mb_strpos($result, 'Time: '));
$echo_result = mb_substr($sub2, mb_strpos($sub2, 'Tests: '));

print($echo_result);    // Lambdaへのレスポンス

if (strpos($echo_result, 'OK') === false) {
    // phpunit実行ログの全文はS3に保存
    $s3_upload_results = phpunit_log_upload_to_s3($result);
    
    if ($s3_upload_results['result'] == 'SUCCESS') {
        print($s3_upload_results['upload_name']);
    } else {
        // S3へのアップロードに失敗した場合、実行結果はSlack等にPOSTしてケア
        ...
    }
}


function phpunit_log_upload_to_s3($log_body) {
    // phpunit実行ログの全文をS3に保存
    ...
}

3. 動作確認

これらの関数は、下記のように動作します。

  • 正常時
    • Pipelineのステージは正常に終了
  • 異常時
    • 実行結果の全文ログはS3に保存
      • (S3保存に失敗した場合は実行結果をSlackへPOST)
    • Pipelineのステージはエラーでストップ
    • 実行情報に、簡単な実行結果一文と、S3ログへのURLを記載

以上の仕組みにより、CodePipelineの任意のステージでPHPUnitを呼び出し、実行結果によって進行/停止させることができました。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です