CodePipelineにテストユニットを設定する

CodePipelineにテストユニットを設定する

CodePipelineにテストユニットを設定する

CodePipelineにテストユニットを追加することができます。
テストユニットはCodeBuildで作成できます。

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

構築する環境

Diagram of setting up Test Units in 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に関する基本的な事項については、以下のページをご確認ください。

あわせて読みたい
CodePipelineを使ってCodeCommitプッシュをトリガーにしてECRにイメージをプッシュする 【CodePipelineを使ってCodeCommitプッシュをトリガーにしてECRにイメージをプッシュする】 CodePipelineを使用することによって、CI/CD構成を構築することができます。...

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

あわせて読みたい
CFNカスタムリソースでS3オブジェクトを作成・削除する 【CloudFormationカスタムリソースを使って、スタック生成/削除時にS3オブジェクトを作成/削除する方法】 CloudFormationカスタムリソースはスタック操作(作成、更新、...
あわせて読みたい
CFNカスタムリソースでECRイメージを削除する 【CloudFormationカスタムリソースでECRイメージを削除する】 CloudFormationを使ってECRを作成し、そこにイメージをプッシュすると、CloudFormationスタック時にエラー...

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スタックを作成します。
スタックの作成および各スタックの確認方法については、以下のページをご確認ください。

あわせて読みたい
CloudFormationのネストされたスタックで環境を構築する 【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を確認します。

Detail of CodePipeline 1.

パイプラインの実行に失敗しています。
これは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を確認します。

Detail of CodePipeline 2.

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

Detail of CodePipeline 3.

テストステージが正常に完了後、ビルドステージも正常に完了しました。

テストのレポートグループを確認します。

Detail of CodeBuild 1.

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

今回実行したテストの詳細を確認します。

Detail of CodeBuild 2.

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

Detail of CodeBuild 3.

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

Detail of CodeBuild 4.

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

ECRリポジトリを確認します。

Detail of ECR Repository 1.

イメージがプッシュされています。テストが正常に完了しましたので、ビルドステージに移行し、Dockerイメージが生成されて、リポジトリにプッシュされたということです。

テスト失敗時

参考にテスト失敗時の挙動を確認します。
関数が出力する文字列を修正し、敢えてテストに失敗させます。

Detail of CodePipeline 4.

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

Detail of CodeBuild 5.

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

続いて詳細を確認します。

Detail of CodeBuild 6.

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

Detail of CodeBuild 7.

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

Detail of CodeBuild 8.

テストケースの詳細ページからも、同様の内容が確認できます。

まとめ

CodePipeline上にテストステージを追加する方法をご紹介しました。