☁️

シンプルな構成のCodePipelineでPHPの静的解析を動かしてみよう

2023/01/28に公開

こんにちは。深緑です。

はじめに

PHPでのシステム開発においてPHPStanによる静的解析を回しています。
以前は解析を手動で実行してましたが、最近は CodeComit + CodePipeline を用いて、
コミットしたら静的解析を実行するようにしました。

CodePipelineの構築はどうせならということでCloudFormationを使いましたが、
AWSのドキュメントにしっくり来るものがなく、作成にちょっと苦労したので共有します。

構築したCodePipelineの動作

構築したCodePipelineの動作

CodePipelineでCodeCommitを監視。
masterブランチにコミットすると反応して、CodeBuildを起動。
CodeBuildはPHPStanを実行します。

この構成をCloudFormationで作成しています。

静的解析でNGが見つかった場合、ビルド失敗→CodePipeline失敗となります。
本記事ではまだやってませんが、SNSを繋げれば失敗時にメールを飛ばしたりできるはずです。

ビルド失敗

成功するとこのようになります。

ビルド成功

テンプレートファイルはこちらです。
https://github.com/satoshi256kbyte/aws-code/blob/master/cloudformation/codepipeline-phpstan.yaml

CodePipelineとCodeBuild、及び実行に必要なバケット・ロールを作っています。
このテンプレートを作る時AWSのドキュメントでサンプルを探したのですが、
CodePipeline+CodeDeployはあるのですがCodeBuildはなかったので少々苦労しました💦
プッシュイベントを使用するようにパイプラインを編集する - AWS公式
テンプレートリファレンス AWS::CodePipeline::Pipeline - AWS公式

CloudFormationテンプレートの使い方

用意したテンプレートは使い回しが効くように以下のパラメータを設けています。

ApplicationName

アプリケーションの名前です。
各リソースの名前の一部として使います。

!Sub ${ApplicationName}-codepipeline

!Subが何を意味するかは以前の記事で触れていますのでご覧ください。

RepositoryName

CodePipelineが監視するCodeCommitのリポジトリ名です。

BranchName

CodePipelineが監視するリポジトリのブランチ名です。

CodeBuild

PHPStanを動かすのに、アプリのソースと同じバージョンのPHPがCodeBuildで動かせる必要があります。
使用可能なランタイムでイメージを変える必要があるのでパラメータ化しています。

Available runtimes - AWS公式
CodeBuild に用意されている Docker イメージ - AWS公式

BuildSpecFile

buildspec.ymlのパスを指定します。
ビルドは環境や状況に応じて動きを変えたい時があります。
そういう場合にbuildspecファイルを切り替えることで動作の分岐を実現できるようファイルパスで対象を指定可能にしています。

OutputArtifactName

CodeBuildが作成するアーティファクトの名前を指定します。
CodeBuildのアーティファクトは私の認識ではCodeDeployで使うものなので、
本当はこの記事のテンプレートではあんまり意味がないパラメータです。

テンプレートのCodeBuildの部分の説明

CodePiplineのアクションにCodeBuildを追加

codepipeline-phpstan.yaml
        - Name: Build
          Actions:
            - Name: !Sub ${ApplicationName}-codebuild
              ActionTypeId:
                Category: Build
                Owner: AWS
                Provider: CodeBuild
                Version: '1'
              RunOrder: 1
              Configuration:
                # BatchEnabled: 'true'
                # CombineArtifacts: 'true'
                ProjectName: !Ref AppPackageBuild
                PrimarySource: SourceOutput
                # EnvironmentVariables: '[{"name":"TEST_VARIABLE","value":"TEST_VALUE","type":"PLAINTEXT"},{"name":"ParamStoreTest","value":"PARAMETER_NAME","type":"PARAMETER_STORE"}]'
              OutputArtifacts:
                - Name: !Ref OutputArtifactName
              InputArtifacts:
                - Name: SourceOutput

CodePipelineにCodeBuildを追加してるのはここになります。
最も大事なのはProjectName: !Ref AppPackageBuildの部分です。
AppPackageBuildというのがこの少し上になります。

codepipeline-phpstan.yaml
  AppPackageBuild:
    Type: 'AWS::CodeBuild::Project'
    Properties:
      Artifacts:
        Type: CODEPIPELINE
      Environment:
        ComputeType: BUILD_GENERAL1_SMALL
        Image: !Ref CodeBuildImage
        Type: LINUX_CONTAINER
        #  PrivilegedMode: True
      Name: !Sub '${ApplicationName}-codebuild'
      ServiceRole: !GetAtt
        - CodeBuildServiceRole
        - Arn
      Source:
        Type: CODEPIPELINE    
        BuildSpec: !Ref BuildSpecFile   

AWS::CodeBuild::Project - AWS公式

ここがCodeBuildの設定を行っている部分です。
ComputeTypeはCodeBuildのコンピューティングタイプです。
とりあえず最小にしてますが、タイプによって金額が違うので適切なものを選ぶのがベターです。
ビルド環境のコンピューティングタイプ - AWS公式
ImageはDockerコンテナイメージを指定します。
パラメータで入力したイメージ識別子を渡しています。
BuildSpecもパラメータのbuildspec.ymlのパスを渡しています。

権限の設定

codepipeline-phpstan.yaml
  CodePipelineServiceRole:
    Type: 'AWS::IAM::Role'
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - codepipeline.amazonaws.com
            Action: 'sts:AssumeRole'
      Path: /
      Policies:
        - PolicyName: !Sub ${ApplicationName}-policy-codepipeline
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - 'codecommit:CancelUploadArchive'
                  - 'codecommit:GetBranch'
                  - 'codecommit:GetCommit'
                  - 'codecommit:GetUploadArchiveStatus'
                  - 'codecommit:UploadArchive'
                Resource: '*'
              - Effect: Allow
                Action:
                  - 'codebuild:BatchGetBuilds'
                  - 'codebuild:StartBuild'
                Resource: '*'
              - Effect: Allow
                Action:
                  - 'cloudwatch:*'
                  - 's3:*'
                Resource: '*'

CodePipelineのロールにCodeBuildを起動するためのポリシーを追加しています。

codepipeline-phpstan.yaml
  CodeBuildServiceRole:
    Type: 'AWS::IAM::Role'
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - codebuild.amazonaws.com
            Action: 'sts:AssumeRole'
      Path: /
      Policies:
        - PolicyName: !Sub ${ApplicationName}-policy-codebuild
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - 'codebuild:BatchGetBuilds'
                  - 'codebuild:StartBuild'
                Resource: '*'
              - Effect: Allow
                Action:
                  - 'cloudwatch:*'
                  - 'logs:*'
                  - 's3:*'
                Resource: '*'

CodeBuildのロールにはログ出力のためのCloudWatchのポリシーと、アーティファクトを格納するためのS3のポリシーを追加しています。
codebuild:BatchGetBuildscodebuild:StartBuildはなんで必要なのかよくわからないのですが私の環境だとないとエラーになります。
原因がわかったら追記します。

テストで使用したPHPソースの説明

https://github.com/satoshi256kbyte/aws-code/tree/master/codepipeline/laravel-sample

このフォルダをCodeCommitのリポジトリにアップしています。
フォルダ内のbuildspec.ymlをパラメータで指定。
これによりCodeBuildがbuildspec.ymlに基づきビルドを実行しています。

buldspec.yml
version: 0.2
phases:
  install:
    runtime-versions:
      php: 8.1
    commands:
        - composer install --working-dir=./
  pre_build:
    commands:
      - composer stan --working-dir=./
  #build: 
  #  commands: 
  #    - sample

アプリのソースがPHP8.1ベースなのでビルド時のランタイムもPHP8.1にします。
なので先述のCodeBuildのImageもPHP8.1が使えるものを指定します。

ビルド前にインストールコマンドを実行できるのでcomposerにインストールコマンドを実行させています。
composer.jsonでPHPStanを指定してるので、
これでPHPStanがインストールされて静的解析の準備が整います。

インストールの事前に実行するコマンド(pre_build)で、PHPStanを実行しています。
これで静的解析でNGが見つかった場合、実際のビルドに入る前にキャンセルすることができます。

まとめ

CodePipelineでの静的解析は途中までやること自体が目的だったのですが、
回し始めてみると意外と仕事をしてくれて最近ではソースに対して少し安心感が増しました(笑)
このままユニットテストとかも組み込んで行きたいと思っています。

なお、GitHub Actionsという選択肢があるのは理解していますが、
しばらくはこのままCodePipelineで行くと思います。

本記事がどこかの開発現場のお役に立てれば幸いです。

補足

composer stanというコマンドはありません。
composer.jsonでstanというショートカットを作ってるのでこのような指定ができます。
PHPStanは意外とメモリを喰うので、私はオプションでメモリを指定するようにしています。
また、他のオプションをつけることもちょいちょいあるので都度指定しなくて良いようにショートカットにしています。

composer.json
    "scripts": {
        "post-autoload-dump": [
            "Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
            "@php artisan package:discover --ansi"
        ],
        "post-update-cmd": [
            "@php artisan vendor:publish --tag=laravel-assets --ansi --force"
        ],
        "post-root-package-install": [
            "@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
        ],
        "post-create-project-cmd": [
            "@php artisan key:generate --ansi"
        ],
        "stan": [
            "phpstan analyze --memory-limit=1G"
        ]
    },

Discussion