
AWSクラウド基盤アーキテクトの松井です。コンテナ、サーバレス、IaC、CI/CDなどの最新技術を駆使し、お客様に最適なAWSアーキテクチャを提案・構築しています。
本コラムでは、3回に渡り、IaC化に最適なAWS CDKを活用して、AWSマネージドサービスであるAWS GlueのCI/CD環境構築を行います。今回は②<構築:環境デプロイ編>として、①<検討編>で検討したAWS GlueのCI/CD構成のうち、[1]~[3]の開発環境をデプロイするまでの構築手順を解説します。
- 検討編(AWS GlueのCI/CD構成の検討)
- 構築:環境デプロイ編(AWS GlueのCI/CD環境の構築とAWS Glueのデプロイ)★今回
- 構築:テスト・承認編(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 CDKプロジェクトの作成
AWS CDK CLIを利用してプロジェクトを作成します。
<コマンド:AWS CDKプロジェクト作成>
cdk init --language pythonapp.pyの修正
app.pyは、AWS CDKから呼び出されるメインプログラムであり、構築対象となるスタックを定義します。今回は、3つのAWS CloudFormationのスタックを作成する想定のため、それらを定義しています。また、GlueJobStackは開発用と本番用の2環境を用意しますので、環境単位でリソースの名称や一部設定を切り替えるため、各クラスの引数に環境ごとに異なるスタック名やパラメーターを指定します。
AWS CloudFormationのスタック
- CicdForGlueStack:AWS GlueをデプロイするためのCI/CD環境を管理するスタック
- GlueJobStack-dev:開発用のAWS Glue環境を管理するスタック
- GlueJobStack-prod:本番用のAWS Glue環境を管理するスタック
<AWS CDKソースファイル:app.py>
#!/usr/bin/env python3
import aws_cdk as cdk
from stack.glue_job_stack import GlueJobStack
from stack.cicd_for_glue_stack import CicdForGlueStack
app = cdk.App()
CicdForGlueStack(app, "CicdForGlueStack")
GlueJobStack(app, "GlueJobStack-dev", 
             param=app.node.try_get_context("dev")
)
GlueJobStack(app, "GlueJobStack-prod",
             param=app.node.try_get_context("prod") 
)
app.synth()cdk.jsonの修正
cdk.jsonには、contextとして予め値を設定し、その値をCDKクラス内で利用できる仕掛けが施されています。app.pyで利用している「app.node.try_get_context()」はその1つで、context内に定義したパラメーター群を取得できる関数です。全環境共通や、環境別に利用したいパラメーターなどはcdk.jsonに定義します。
<定義ファイル:cdk.json>
{
…前略
  "context": {
    "common": {
      "deploy_bucket_name": "aws-glue-scripts-20231031",
      "manual_approval_to_address": "xxx @fsi.co.jp"
    },
    "dev": {
      "env_name": "dev",
      "stack_name": "GlueJobStack-dev"
    },
    "prod": {
      "env_name": "prod",
      "stack_name": "GlueJobStack-prod"
    },
…後略
}
requirements.txtの修正
AWS CDKのコマンドでAWS CloudFormationのテンプレートを生成するために必要な依存パッケージを定義します。デフォルトで定義されているパッケージだけで構築できることもありますが、今回は‘aws-cdk-lib’に含まれていないAWSリソース(AWS CloudFormation)用のパッケージを利用する必要があるため、追記します。
<パッケージ定義ファイル:requirements.txt>
aws-cdk-lib==2.103.1
constructs>=10.0.0,<11.0.0
aws-cdk.aws-glue-alpha==2.103.1a0 #追記glue_job_stack.pyの作成
今回作成するAWS Glueジョブは、PythonShellの環境を利用します。環境単位でジョブ名やジョブのソースコードの参照先が変わるように設定していますが、その他は基本的にデフォルトの設定になっています。
<AWS CDKソースファイル:stack/glue_job_stack.py>
from aws_cdk import (
    Stack,
    aws_s3 as s3,
    aws_glue_alpha as glue,
)
from constructs import Construct
class GlueJobStack(Stack):
    def __init__(self, scope: Construct, construct_id: str, param, **kwargs) -> None:
        super().__init__(scope, construct_id, **kwargs)
        # Get S3 Bucket Arn
        bucket_name = self.node.try_get_context("common")["deploy_bucket_name"]
        bucket = s3.Bucket.from_bucket_attributes(self, "PythonShellBucket",
            bucket_arn=f"arn:aws:s3:::{bucket_name}"
        )
        # Create Glue Job
        env_name=param["env_name"]
        glue.Job(self, "PythonShellJob",
            job_name=f"{env_name}_pythonshell_job",
            executable=glue.JobExecutable.python_shell(
                glue_version=glue.GlueVersion.V3_0,
                python_version=glue.PythonVersion.THREE_NINE,
                script=glue.Code.from_bucket(bucket, f"{env_name}/pythonscript/script.py")
            ),
            default_arguments={
                "--ENVIROMENT": env_name,
            },
            description="an example Python Shell job"
        )script.pyの作成
AWS Glueジョブで実行するスクリプトファイルを作成します。今回はあくまでもCI/CD構成の紹介をメインとしているため、スクリプト自体は単純に環境名を出力するだけの簡単なものを用意しました。
<AWS Glueジョブソースファイル:pythonscript/glue_job_stack.py>
import sys
from awsglue.utils import getResolvedOptions
args = getResolvedOptions(sys.argv, ["ENVIROMENT"])
print("Environment Name: " + args["ENVIROMENT"])cicd_for_glue_stack.pyの作成
こちらは本コラムのメインのソースコードです。100行ほどのコーディングで、開発環境をデプロイするまでに必要なCI/CDのリソースを定義できます。以下のコードを見ていただくとわかるのですが、AWS CDKを利用することで可読性の高い、必要最低限のAWSリソースのみを定義しています。AWS CloudFormationの場合は、IAM RoleやAmazon EventBridge、Amazon S3などCI/CDに必要なAWSリソースをすべて定義する必要がありましたが、AWS CDKはそれらを含めて構築するようにライブラリ(Construct)が提供されているため、コーディングの負荷が軽減しています。
ちなみに、以下のソースコードをcdk synthコマンドでAWS CloudFormationテンプレートに出力したところ、約1,000行のYAMLファイルが生成されました。
<AWS CDKソースファイル:stack/glue_job_stack.py>
from aws_cdk import (
    Stack,
    aws_s3 as s3,
    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]ビルドアクション(開発環境向け)
        stack_name=self.node.try_get_context("dev")["stack_name"]
        env_name=self.node.try_get_context("dev")["env_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]
        )
         [3]デプロイアクション(開発環境向け)
        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
            ]
        )
CI/CD環境のデプロイ
必要なソースコードの準備が整ったので、実際にCI/CDの環境を構築します。CI/CD環境の構築は、以下のコマンドで実行します。
cdk deploy CicdForGlueStackデプロイが完了すると、cicd_for_glue_stack.pyに定義した通りに、AWS CodePipelineにパイプラインが作成されます。Sourceブロックが失敗していますが、これはまだ対象のCodeCommitにソースコードをプッシュしていないため、想定通りのエラーです。
<マネージメントコンソール:AWS CodePipeline パイプライン画面>

ここまでの手順でCI/CD環境の構築は完了です。
動作確認
実際にCI/CD環境を動かして、AWS Glueをデプロイします。
CI/CDのトリガーは、CodeCommit(Git)リポジトリへのプッシュになるので、ローカル環境からソースをプッシュします。
<コマンド:Git>
git remote add origin codecommit::ap-northeast-1://glue-repository
git add -A
git commit -m first-commit
git pushリポジトリにプッシュ後、AWS CodePipelineの画面を見てみると、デプロイに成功しているようです。
<マネージメントコンソール:AWS CodePipeline パイプライン画面>

さらにAWS Glueジョブの管理画面を確認してみると、AWS Glueジョブとソースコードが想定通りデプロイできていました。
<マネージメントコンソール:AWS Glue ジョブ詳細画面>

CI/CDを導入しない場合、AWS Glueジョブの各種設定の更新とAmazon S3へのソースコードのアップロードをそれぞれ行う必要がありましたが、CI/CDを導入することにより、リポジトリにコードをプッシュするだけでそれらを行えるようになりました。開発時は何度も更新するため、CI/CDを利用して効率的に作業できると開発が捗りますし、AWS CDKでソースコードを100行程度書くだけで構築できますので、ぜひ導入をご検討ください。
次回、③<テスト+承認編>では、テストの自動化や承認工程を加えてさらに実用的なCI/CDの構築手順を紹介しますので、ぜひご覧ください。
参考URL
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/
次の記事はこちら
AWS CDKを活用した効率的なAWS GlueのCI/CD環境構築③<構築:テスト・承認編>
前の記事はこちら
AWS CDKを活用した効率的なAWS GlueのCI/CD環境構築①<検討編>
富士ソフトのAWS関連サービスについて、詳しくはこちら
アマゾンウェブサービス(AWS) 




