CodePipelineにテストユニットを設定する
CodePipelineにテストユニットを追加することができます。
テストユニットはCodeBuildで作成できます。
今回はDockerイメージを作成し、ECRリポジトリにプッシュするパイプラインをCodePipelineで構築します。
その過程にテストユニットを追加することを目的とします。
構築する環境

CodePipelineを構成し、3つのリソースを連携させます。
1つ目はCodeCommitです。
CodeCommitはCodePipelineのソースステージを担当します。
Gitリポジトリとして使用します。
2つ目はCodeBuildです。
こちらのCodeBuildはCodePipelineのテストステージを担当します。
今回のテスト対象はPythonスクリプトとし、テストで使用するツールはpytestとします。
3つ目もCodeBuildです。
こちらのCodeBuildはCodePipelineのビルドステージを担当します。
CodeCommitにプッシュされたコードから、Dockerイメージをビルドします。
ビルドしたイメージをECRにプッシュします。
SSMパラメータストアにDockerHubアカウント情報を保存します。
DockerBuildでイメージを生成する際に、DockerHubにサインインした上でベースイメージをプルするために、これらを使用します。
CodePipelineが開始されるきっかけですが、CodeCommitへのプッシュを条件とします。
具体的には、EventBridgeに上記を満たすルールを用意します。
CloudFormationテンプレートファイル
上記の構成をCloudFormationで構築します。
以下のURLにCloudFormationテンプレートを配置しています。
https://github.com/awstut-an-r/awstut-fa/tree/main/081
テンプレートファイルのポイント解説
本ページはCodePipelineで作成したパイプライン内に、テストユニットを追加することを目的とします。
CodePipelineに関する基本的な事項については、以下のページをご確認ください。

CloudFormationカスタムリソースを使用して、CloudFormationスタック削除時に、自動的にS3バケット内のオブジェクトと、ECRリポジトリ内のイメージを削除します。
詳細につきましては、以下のページをご確認ください。


ReportGroup
Resources:
  CodeBuildReportGroup:
    Type: AWS::CodeBuild::ReportGroup
    Properties:
      DeleteReports: true
      ExportConfig:
        ExportConfigType: NO_EXPORT
      Name: !Sub "${CodeBuildProject1}-${ReportName}"
      Type: TEST
Code language: YAML (yaml)CodeBuildでテストユニットを実行するためには、レポートグループを作成します。
Typeプロパティでレポートグループのタイプを設定します。
「CODE_COVERAGE」を指定すると、コードカバレッジ、つまりコード網羅率のレポートを作成します。
「TEST」を指定すると、通常のテストレポートを作成します。
今回は後者を選択します。
DeleteReportsプロパティはレポートの削除に関するパラメータです。
これを有効化すると、グループ内部にレポートが残っていたとしても、グループとレポートをまとめて削除できるようになります。
ExportConfigはレポートグループのエクスポートに関するパラメータです。
生のレポートデータをS3バケットに出力することができます。
今回はエクスポートしません。
CodeBuild
Resources:
  CodeBuildProject1:
    Type: AWS::CodeBuild::Project
    Properties:
      Artifacts:
        Type: CODEPIPELINE
      Cache:
        Type: NO_CACHE
      Environment:
        ComputeType: !Ref ProjectEnvironmentComputeType
        EnvironmentVariables:
          - Name: REPORT_NAME
            Type: PLAINTEXT
            Value: !Ref ReportName
        Image: !Ref ProjectEnvironmentImage
        ImagePullCredentialsType: CODEBUILD
        Type: !Ref ProjectEnvironmentType
        PrivilegedMode: true
      LogsConfig:
        CloudWatchLogs:
          GroupName: !Ref LogGroup
          Status: ENABLED
        S3Logs:
          Status: DISABLED
      Name: !Sub "${Prefix}-project-01"
      ServiceRole: !GetAtt CodeBuildRole.Arn
      Source:
        Type: CODEPIPELINE
        BuildSpec: !Sub |
          version: 0.2
          phases:
            install:
              runtime-versions:
                python: 3.7
              commands:
                - pip3 install pytest
                - pip3 install bottle
            build:
              commands:
                - python -m pytest --junitxml=reports/pytest_reports.xml
          reports:
            $REPORT_NAME:
              files:
                - pytest_reports.xml
              base-directory: reports
              file-format: JUNITXML
      Visibility: PRIVATE
Code language: YAML (yaml)Environmentプロパティでテストを実行する環境を設定します。
ComputeTypeプロパティでビルド環境のスペックを指定します。
今回は最小構成(メモリ4GB、2vCPU、ストレージ」50GB)の「BUILD_GENERAL1_SMALL」を指定します。
https://docs.aws.amazon.com/ja_jp/codebuild/latest/userguide/build-env-ref-compute-types.html
EnvironmentVariablesプロパティでビルド環境で使用できる環境変数を設定できます。
今回は先述のレポートグループの名前を設定します。
Imageプロパティはビルド環境を作成するためのDockerイメージを指定します。
今回はARM版のAmazon Linux 2である「aws/codebuild/amazonlinux2-aarch64-standard:2.0」を指定します。
https://docs.aws.amazon.com/ja_jp/codebuild/latest/userguide/build-env-ref-available.html
ImagePullCredentialsTypeプロパティはイメージをプルするための権限を設定します。
「CODEBUILD」を設定すると、独自のクリデンシャルを使用することになります。
Typeプロパティでビルド環境のタイプを設定します。
今回はARM環境を意味する「ARM_CONTAINER」を設定します。
PrivilegedModeプロパティはDockerデーモンの実行に関するパラメータです。
以下の引用の通り、今回は有効化します。
Enables running the Docker daemon inside a Docker container. Set to true only if the build project is used to build Docker images. Otherwise, a build that attempts to interact with the Docker daemon fails.
AWS::CodeBuild::Project Environment
Sourceプロパティでbuildspec.ymlの内容を定義します。
pytestを使用して、Pythonスクリプトをテストします。
今回は以下のページを参考に設定を行いました。
https://docs.aws.amazon.com/ja_jp/codebuild/latest/userguide/test-report-pytest.html
以下がCodeBuild用のIAMロールです。
Resources:
  CodeBuildRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - codebuild.amazonaws.com
            Action:
              - sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryPowerUser
      Policies:
        - PolicyName: PipelineExecutionPolicy
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - ssm:GetParameters
                Resource:
                  - !Sub "arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/${SSMParameterDockerHubPassword}"
                  - !Sub "arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/${SSMParameterDockerHubUsername}"
              - Effect: Allow
                Action:
                  - s3:PutObject
                  - s3:GetObject
                  - s3:GetObjectVersion
                  - s3:GetBucketAcl
                  - s3:GetBucketLocation
                Resource:
                  - !Sub "arn:aws:s3:::${BucketName}"
                  - !Sub "arn:aws:s3:::${BucketName}/*"
              - Effect: Allow
                Action:
                  - codebuild:CreateReportGroup
                  - codebuild:CreateReport
                  - codebuild:UpdateReport
                  - codebuild:BatchPutTestCases
                  - codebuild:BatchPutCodeCoverages
                Resource:
                  - !Sub "arn:aws:codebuild:${AWS::Region}:${AWS::AccountId}:report-group/*"
              - Effect: Allow
                Action:
                  - logs:CreateLogGroup
                  - logs:CreateLogStream
                  - logs:PutLogEvents
                Resource:
                  - !GetAtt LogGroup.Arn
                  - !Sub
                    - "${LogGroupArn}:log-stream:*"
                    - LogGroupArn: !GetAtt LogGroup.Arn
Code language: YAML (yaml)レポートグループに関する権限を付与します。
CodePipeline
Resources:
  Pipeline:
    Type: AWS::CodePipeline::Pipeline
    Properties:
      ArtifactStore:
        Location: !Ref BucketName
        Type: S3
      Name: !Ref Prefix
      RoleArn: !GetAtt CodePipelineRole.Arn
      Stages:
        - Actions:
            - ActionTypeId:
                Category: Source
                Owner: AWS
                Provider: CodeCommit
                Version: 1
              Configuration:
                BranchName: !Ref BranchName
                OutputArtifactFormat: CODE_ZIP
                PollForSourceChanges: false
                RepositoryName: !GetAtt CodeCommitRepository.Name
              Name: Source
              OutputArtifacts:
                - Name: !Ref PipelineSourceArtifact
              Region: !Ref AWS::Region
              RunOrder: 1
          Name: Source
        - Actions:
            - ActionTypeId:
                Category: Test
                Owner: AWS
                Provider: CodeBuild
                Version: 1
              Configuration:
                ProjectName: !Ref CodeBuildProject1
              InputArtifacts:
                - Name: !Ref PipelineSourceArtifact
              Name: Test
              OutputArtifacts: []
              Region: !Ref AWS::Region
              RunOrder: 1
          Name: Test
        - Actions:
            - ActionTypeId:
                Category: Build
                Owner: AWS
                Provider: CodeBuild
                Version: 1
              Configuration:
                ProjectName: !Ref CodeBuildProject2
              InputArtifacts:
                - Name: !Ref PipelineSourceArtifact
              Name: Build
              OutputArtifacts:
                - Name: !Ref PipelineBuildArtifact
              Region: !Ref AWS::Region
              RunOrder: 1
          Name: Build
Code language: YAML (yaml)Stagesプロパティにテストステージを定義します。
Configurationプロパティ内で、テストステージの詳細設定を行います。
ProjectNameプロパティで、先述のCodeBuildを指定します。
InputArtifactsプロパティで、テスト対象のスクリプト等を指定します。今回はSourceステージのアーティファクトを指定します。
(参考)アプリコンテナ用ファイル
Dockerfile
FROM amazonlinux
RUN yum update -y && yum install python3 python3-pip -y
RUN pip3 install bottle
COPY main.py ./
CMD ["python3", "main.py"]
EXPOSE 8080
Code language: Dockerfile (dockerfile)アプリコンテナ用のイメージは、Amazon Linux 2をベースとして作成します。
Python製WebフレームワークのBottleを使用します。
ですからPythonおよびpipをインストール後、これをインストールします。
アプリロジックを記載したPythonスクリプト(main.py)をコピーし、これを実行するように設定します。
先述の通り、アプリは8080/tcpでHTTPリクエストを待ち受けますので、このポートを公開します。
main.py
from bottle import route, run
@route('/')
def hello():
  return 'Hello CodePipeline.'
if __name__ == '__main__':
  run(host='0.0.0.0', port=8080)
Code language: Python (python)Bottleを使用して、簡易的なWebサーバを構築します。
8080/tcpでHTTPリクエストを待ち受け、「Hello CodePipeline.」を返すという単純な構成です。
test_main.py
import pytest
from main import hello
@pytest.mark.parametrize(('expected',), [
  ('Hello CodePipeline.',),
])
def test_hello(expected):
  assert hello() == expected
Code language: Python (python)pytestでmain.py内に定義したhello関数をテストするためのスクリプトです。
hello関数を実行し、「Hello CodePipeline.」の文字列が返ってきたら成功という内容です。
環境構築
CloudFormationを使用して、本環境を構築し、実際の挙動を確認します。
CloudFormationスタックを作成し、スタック内のリソースを確認する
CloudFormationスタックを作成します。
スタックの作成および各スタックの確認方法については、以下のページをご確認ください。

各スタックのリソースを確認した結果、今回作成された主要リソースの情報は以下の通りです。
- ECR:fa-081
- CodeCommit:fa-081
- CodeBuild1:fa-081-project-01
- CodeBuild2:fa-081-project-02
- CodePipeline:fa-081
AWS Management Consoleから各リソースを確認します。
CodePipelineを確認します。

パイプラインの実行に失敗しています。
これはCloudFormationスタック作成時に、CodeCommitが作成されたことがきっかけとして、パイプラインが実行されたためです。
現時点ではCodeCommitにコードをプッシュしていないため、パイプライン実行の過程でエラーが発生しました。
作成されているステージに注目します。
Testという名前で、テストユニット用のステージが作成されています。
CodeCommitにコードがプッシュされると、テストユニットが実行され、正常な動作が確認できれば、Dockerイメージをビルドし、ECRリポジトリにプッシュするという流れになります。
動作確認
テスト成功時
準備が整いましたので、CodeCommitにコードをプッシュします。
まずCodeCommitをプルします。
$ git clone https://git-codecommit.ap-northeast-1.amazonaws.com/v1/repos/fa-081
Cloning into 'fa-081'...
warning: You appear to have cloned an empty repository.
Code language: Bash (bash)空のリポジトリがプルされました。
リポジトリに3つのファイル(Dockerfile、main.py、test_main.py)を加えます。
$ ls -al
total 12
drwxrwxr-x 3 ec2-user ec2-user  71 Aug 21 11:24 .
drwxrwxr-x 6 ec2-user ec2-user 125 Aug 21 11:24 ..
-rw-rw-r-- 1 ec2-user ec2-user 187 Aug 12 11:01 Dockerfile
drwxrwxr-x 7 ec2-user ec2-user 119 Aug 21 11:24 .git
-rw-rw-r-- 1 ec2-user ec2-user 681 Aug 21 08:07 main.py
-rw-rw-r-- 1 ec2-user ec2-user 233 Aug 21 08:34 test_main.py
Code language: Bash (bash)3ファイルをCodeCommitにプッシュします。
$ git add .
$ git commit -m 'first commit'
...
 3 files changed, 51 insertions(+)
 create mode 100644 Dockerfile
 create mode 100644 main.py
 create mode 100644 test_main.py
$ git push
...
 * [new branch]      master -> master
Code language: Bash (bash)正常にプッシュできました。
しばらく待機した後、改めてCodePipelineを確認します。

パイプラインが開始されました。
Sourceステージは正常に完了し、テスト(Test)ステージに到達しました。
現在は「In progress」とあり、テストユニットを実行中であることがわかります。
しばらく待機します。

テストステージが正常に完了後、ビルドステージも正常に完了しました。
テストのレポートグループを確認します。

レポートグループが作成されています。
テストユニット実行結果の概要が確認できます。
今回実行したテストの詳細を確認します。

グラフや表でテスト結果が表示できます。

テスト時のフェーズ移行の過程も確認できます。
今回は全て正常に完了しました。

テスト実行時の詳細なログも確認することができます。
ECRリポジトリを確認します。

イメージがプッシュされています。テストが正常に完了しましたので、ビルドステージに移行し、Dockerイメージが生成されて、リポジトリにプッシュされたということです。
テスト失敗時
参考にテスト失敗時の挙動を確認します。
関数が出力する文字列を修正し、敢えてテストに失敗させます。

CodePipeline上で、テストに失敗したことがわかります。

レポートグループを確認すると、失敗したテストの概要がわかります。
続いて詳細を確認します。

ビルドフェーズで失敗していることがわかります。

ログを見ると、失敗した原因が確認できます。
返ってくる文字列が期待通りのものではなかったという内容です。

テストケースの詳細ページからも、同様の内容が確認できます。
まとめ
CodePipeline上にテストステージを追加する方法をご紹介しました。