Twitter
Facebook
Hatena
AWS CDKを活用した効率的なAWS GlueのCI/CD環境構築③<構築:テスト・承認編>

AWSクラウド基盤アーキテクトの松井です。コンテナ、サーバレス、IaC、CI/CDなどの最新技術を駆使し、お客様に最適なAWSアーキテクチャを提案・構築しています。

本コラムでは、3回に渡り、IaC化に最適なAWS CDKを活用して、AWSマネージドサービスであるAWS GlueのCI/CD環境構築を行います。今回は②<構築:環境デプロイ編>に続き、③<構築:テスト・承認編>としてAWS GlueのCI/CD構成のうち、[4]~[7]の構築手順を解説します。

  1. 検討編(AWS GlueのCI/CD構成の検討)
  2. 構築:環境デプロイ編(AWS GlueのCI/CD環境の構築とAWS Glueのデプロイ)
  3. 構築:テスト・承認編(AWS GlueのCI/CD環境に自動テストと承認を追加)★今回

本コラムで構築するCI/CD環境の構成

②構築:環境デプロイ編で構築

[1] ソースアクション
 AWS GlueジョブのスクリプトコードとAWS CDKのコードをリポジトリにプッシュ
[2] ビルドアクション(開発環境)
 AWS CDKのコードを開発環境向けのAWS CloudFormationテンプレートに変換
[3] デプロイアクション(開発環境)
 AWS CloudFormationのテンプレートを利用して開発環境向けにAWS Glueジョブをデプロイ

③構築:テスト・承認編で構築

[4] テストアクション(開発環境)
 AWS Glueジョブを実行
[5]承認アクション
 テスト結果の確認+本番環境へのデプロイ承認(手動作業)
[6] ビルドアクション(本番環境)
 AWS CDKのコードを本番環境向けのAWS CloudFormationテンプレートに変換
[7] デプロイアクション(本番環境)
 AWS CloudFormationのテンプレートを利用して本番環境向けにAWS Glueジョブをデプロイ

構築手順

テストコードの作成

前回デプロイしたAWS Glueジョブが問題なく動作するかを確認するために、テストコードを作成します。以下のテストコードは、AWS SDKを利用してAWS Glue ジョブを開始し、そのジョブが完了するまで待機した後、ジョブのステータスが‛SUCCEEDED’となっているかを確認するためのソースです。
このテストコードは、Amazon Web Services ブログ『AWS Developer Toolsを使用したサーバレスなAWS Glue ETLアプリケーションの継続的インテグレーションとデリバリの実装』のサンプルコードを参考にしています。

AWS Developer Toolsを使用したサーバレスなAWS Glue ETLアプリケーションの継続的インテグレーションとデリバリの実装
https://aws.amazon.com/jp/blogs/news/implement-continuous-integration-and-delivery-of-serverless-aws-glue-etl-applications-using-aws-developer-tools/

<テストコード:tests/gluejob/gluejobtest.py>

import os
import unittest
import time
import boto3

TEST_JOB_NAME = os.environ['TEST_JOB_NAME']

glue = boto3.client('glue')
client = boto3.client('cloudformation')

def runJob(jobname):
    response = glue.start_job_run(JobName=jobname)
    jobRunid = response['JobRunId']
    response = glue.get_job_run(JobName=jobname,RunId=jobRunid)
    state = response['JobRun']['JobRunState']
    print("state " + state)
    while state == 'RUNNING':
        time.sleep(60)
        response = glue.get_job_run(JobName=jobname,RunId=jobRunid)
        state = response['JobRun']['JobRunState']
        print("state " + state)
    print("final state " + state)
    return state

class MyTestCase(unittest.TestCase):
    def test_data_lake(self):
        self.assertEqual(runJob(TEST_JOB_NAME), 'SUCCEEDED')    

if __name__ == '__main__':
   unittest.main()

また、テストコードを実行するために必要なモジュールもrequirements.txtに定義しておきます。

<パッケージ定義ファイル:tests/gluejob/requirements.txt>

pytest-cov
pytest
boto3

cicd_for_glue_stack.pyの修正

本番環境向けにCI/CD環境を構築するための修正を行います。
主な修正ポイントは以下の3点です

  • 開発環境向けのビルド・デプロイ([2][3])と本番環境のビルド・デプロイ([6][7])は、構築先の環境が異なります。構築内容は同様なため、‛set_build_deploy_actions()’関数を用意して、異なる値を渡すだけでそれぞれ構築できるようにしています。AWS CDKはこのように効率的に環境を定義できることが素晴らしいと思います。
  • [4]では、先ほど作成したテストコードを実行するコマンドをAWS CodeBuild上で実行するように定義しています。また、AWS CodeBuildでは、ビルド時に実施したテスト結果をレポート出力できるので、今回はその機能を有効にしています。
  • [5]の承認アクションでは、‛cdk.jcon’のコンテキストで予め定義したメールアドレス宛にメールを送信するように定義しています。

<AWS CDKソースファイル:stack/cicd_for_glue_stack.py>

from aws_cdk import (
    Stack,
    aws_s3 as s3,
    aws_sns as sns,
    aws_iam as iam,
    aws_codebuild as codebuild,
    aws_codecommit as codecommit,
    aws_codepipeline as codepipeline,
    aws_codepipeline_actions as codepipeline_actions,
)
from constructs import Construct

class CicdForGlueStack(Stack):

    def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
        super().__init__(scope, construct_id, **kwargs)

        # [事前準備1] リポジトリ+S3バケット作成
        repository = codecommit.Repository(self, "Repository",
            repository_name="glue-repository"
        )
        deploy_bucket = s3.Bucket(self, "BucketToPythonShell",
            bucket_name=self.node.try_get_context("common")["deploy_bucket_name"]
        )

        # [事前準備2] CodePipeline作成
        pipeline = codepipeline.Pipeline(self, "Pipeline")

        # CI/CDフロー
        # [1] ソースアクション
        source_output=codepipeline.Artifact("source_artifact")
        source_action=codepipeline_actions.CodeCommitSourceAction(
            repository=repository,
            branch="master",
            action_name="Source-CodeCommit",
            output=source_output,
            trigger=codepipeline_actions.CodeCommitTrigger.EVENTS
        )
        pipeline.add_stage(
            stage_name="Source",
            actions=[source_action]
        )

        # [2,3] ビルド+デプロイアクション(開発環境向け)>
        stack_name=self.node.try_get_context("dev")["stack_name"]
        env_name=self.node.try_get_context("dev")["env_name"]
        self.set_build_deploy_actions(pipeline, source_output, deploy_bucket, env_name, stack_name)

        # [4] テストアクション
        report_group = codebuild.ReportGroup(self, "ReportGroup")
        test_project = codebuild.PipelineProject(self, "Test",
            environment=codebuild.BuildEnvironment(
                build_image=codebuild.LinuxBuildImage.from_docker_registry(
                    "public.ecr.aws/glue/aws-glue-libs:glue_libs_3.0.0_image_01"
                ),
                environment_variables={
                    "TEST_JOB_NAME":{
                        "value": "dev_pythonshell_job"
                    },
                    "JUNIT_XML":{
                        "value": "junit_coverage.xml"
                    },
                    "TEST_FOLDER_PATH":{
                        "value": "tests/gluejob/"
                    }
                }
            ),
            build_spec=codebuild.BuildSpec.from_object({
                "version": "0.2",
                "phases": {
                    "install": {
                        "commands": [
                            "pip3 install -r ${TEST_FOLDER_PATH}requirements.txt"
                        ]
                    },
                    "build": {
                        "commands": [
                            "python3 -m pytest ${TEST_FOLDER_PATH}gluejobtest.py --junitxml=${JUNIT_XML}"
                        ]
                    }
                },
                "reports": {
                    report_group.report_group_arn: {
                        "files": "**/*",
                        "base-directory": "${CODEBUILD_SRC_DIR}",
                        "file-format": "JunitXml",
                        "discard-paths": "yes"
                    }
                }
            })
        )
        test_project.add_to_role_policy(iam.PolicyStatement.from_json(
            {
                "Effect": "Allow",
                "Action": [
                    "codebuild:CreateReportGroup",
                    "codebuild:CreateReport",
                    "codebuild:UpdateReport",
                    "codebuild:BatchPutTestCases",
                    "glue:GetJobRun",
                    "glue:StartJobRun"
                ],
                "Resource": "*"
            }
        ))
        test_action =  codepipeline_actions.CodeBuildAction(
            action_name="Test-CodeBuild",
            project=test_project,
            input=source_output,
        )
        pipeline.add_stage(
            stage_name="TestForDev",
            actions=[test_action]
        )

        # [5] 承認アクション
        manual_approval_action = codepipeline_actions.ManualApprovalAction(
            action_name="Approve",
            notification_topic=sns.Topic(self, "Topic"),
            notify_emails=[
                self.node.try_get_context("common")["manual_approval_to_address"]
            ],
            additional_information="additional info"
        )
        pipeline.add_stage(
            stage_name="Approve",
            actions=[manual_approval_action]
        
        )

        # [6,7]ビルド+デプロイアクション(本番環境向け)
        stack_name=self.node.try_get_context("prod")["stack_name"]
        env_name=self.node.try_get_context("prod")["env_name"]
        self.set_build_deploy_actions(pipeline, source_output, deploy_bucket, env_name, stack_name)

    def set_build_deploy_actions(self, pipeline, source_output, deploy_bucket, env_name, stack_name):
        build_project = codebuild.PipelineProject(self, f"Build_{env_name}",
            environment=codebuild.BuildEnvironment(
                build_image=codebuild.LinuxBuildImage.STANDARD_7_0,
                environment_variables={
                    "DEPLOY_STACK_NAME":{
                        "value": stack_name
                    },
                    "ENV_NAME":{
                        "value": env_name
                    }
                }
            ),
            build_spec=codebuild.BuildSpec.from_object({
                "version": "0.2",
                "phases": {
                    "install": {
                        "runtime-version": {
                            "python": 3.9
                        },
                        "commands": [
                            "npm install -g aws-cdk",
                            "pip3 install -r requirements.txt"
                        ]
                    },
                    "build": {
                        "commands": [
                            "cdk synth ${DEPLOY_STACK_NAME} > template.yaml"
                        ]
                    }
                },
                "artifacts": {
                    "files": [
                        "template.yaml",
                        "pythonscript/script.py"
                    ]
                }
            })
        )
        build_output = codepipeline.Artifact(f"build_output_{env_name}")
        build_action =  codepipeline_actions.CodeBuildAction(
            action_name=f"Build-CodeBuild-{env_name}",
            project=build_project,
            input=source_output,
            outputs=[build_output]
        )
        pipeline.add_stage(
            stage_name=f"Build_{env_name}",
            actions=[build_action]
        )

        deploy_stage_cloudformation = codepipeline_actions.CloudFormationCreateUpdateStackAction(
            action_name=f"Deploy-Cfn-{env_name}",
            stack_name=stack_name,
            admin_permissions=True,
            template_path=build_output.at_path("template.yaml"),
            run_order=1
        )
        deploy_stage_s3 = codepipeline_actions.S3DeployAction(
            action_name=f"Deploy-S3-{env_name}",
            bucket=deploy_bucket,
            input=build_output,
            extract=True,
            object_key=f"{env_name}",
            run_order=2
        )
        pipeline.add_stage(
            stage_name=f"Deploy-{env_name}",
            actions=[
                deploy_stage_cloudformation,
                deploy_stage_s3
            ]
        )

ちなみに、上記AWS CDKで定義したコード数は約200行ですが、cdk synthコマンドでAWS CloudFormationテンプレート出力してみたところ、約1,800行のYAMLファイルが生成されました。<検討・解説編>でも比較しましたが、やはりAWS CDKを利用するとコード数が少なく済むため、効率的に環境を構築できます。

CI/CD環境再デプロイ

CI/CDの環境を更新したいので、再びコマンドを利用して環境をデプロイします。
(実行コマンドは②<構築:環境デプロイ編>をご参照ください)
デプロイが完了すると、[4]以降がAWS CodePipelineに構築されます。


<マネージメントコンソール:AWS CodePipeline パイプライン画面>

また、[5]の承認アクションで指定したメールアドレス宛に、Amazon SNSからの通知の承諾メールが送信されるため、予め承認(Confirm)しておきます。

<Amazon SNS承認メール>

ここまでの手順でCI/CD環境の修正は完了です。

動作確認

ここからは、修正を加えたCI/CD環境を動かして、自動テストや承認プロセスを実際に確認してみます。
テストコードなど追加したファイルをリポジトリにプッシュし、CI/CD環境を動かします。(プッシュ方法は②<構築:環境デプロイ編>をご参照ください)
リポジトリにプッシュ後、AWS CodePipelineの画面を確認すると、テストアクションが成功していたので、AWS Glueジョブの実行履歴とAWS CodeBuildから作成されたレポートを確認します。

<マネージメントコンソール:AWS Glueジョブの実行履歴>

<マネージドコンソール:AWS CodeBuildのテストレポート>

AWS Glueジョブは無事に成功し、その内容がテストレポートに反映されていました。今回のテストでは、AWS Glueジョブの実行結果のみを確認するテストケースに限定して実装したため、非常に簡素なレポートになっています。Amazon S3へのファイル出力やAmazon DynamoDBへのデータ書き込みなどの処理が含まれる場合には、それらが正常に行われたかどうかを確認するテストコードを追加して、テストレポートに出力されるようにすると、効率的にテスト結果を確認できます。この機能はテストを実施した証跡として残すことができるので、非常に有効だと思います。

メールの受信ボックスには、AWS CodePipelineの承認依頼メールが届きました。

<AWS CodePipelineの承認依頼メール>

メール本文内のURLをクリックすると、AWS CodePipelineの画面に遷移します。Approveが「承認を保留中」になっていますので、「レビュー」を押下します。

<マネージメントコンソール:AWS CodePipeline パイプライン画面(一部)>

AWS Glueジョブの画面もAWS CodeBuildで作成したレポートも確認済みのため、レビュー画面で「承認します」を押下します。

<マネージメントコンソール:AWS CodePipeline レビュー画面>

※ここで「却下します」を選択すると本番環境のデプロイは行われず、このAWS CodePipelineのフローは終了します。

<マネージメントコンソール:AWS CodePipeline パイプライン画面(一部)>

Approveが「承認されました」に変わりました。これでAWS CDKで定義した通りに本番環境のデプロイ処理まで実施できました。

さいごに

本コラムでは、①<検討編>②<構築:環境デプロイ編>、③<構築:テスト・承認編>の全3回に渡りAWS CDKによるAWS GlueのCI/CD環境構築方法をご紹介しました。
AWSはCI/CD向けのサービスが充実しており、AWS CDKを利用すれば効率的にCI/CDの環境を構築できます。開発効率向上のため、ぜひCI/CDの導入をご検討ください。

前の記事はこちら
AWS CDKを活用した効率的なAWS GlueのCI/CD環境構築②<構築:環境デプロイ編>

富士ソフトのAWS関連サービスについて、詳しくはこちら
アマゾンウェブサービス(AWS)

 

 

この記事の執筆者

松井 美佳Mika Matsui

エリア事業本部
西日本支社 インテグレーション&ソリューション部
第1技術グループ
リーダー / エキスパート

AWS クラウド