AWS GlueジョブをCloudFormationで構築する

AWS GlueのGlueジョブやGlue ConnectionをCloudFormationで構築する方法を書いてみました。必須のもの以外の諸々も全て作成したのでかなりの量です。


やりたいこと

  • AWS Glueジョブとそれに必要なものをCloudFormationで構築する
    • ymlで記述します


前提

  • 転送元データソースはMySQL(EC2,RDS,Aurora問わず)、転送先データソースはS3を想定しています
    • そうでない場合でもある程度は使いまわせます
  • MySQL側のスキーマ情報を使用するため、Glue Data Catalogは使用しません
  • デプロイをする環境ですが、UNIX系を想定しています(MacもOK)
  • シェルスクリプトを用いますが、jqというコマンド(モジュール)を使用するので、インストールが必要です


CloudFormationで構築する概要

  • Glueジョブで使用するIAMロール・IAMポリシー
  • S3バケット(Glueジョブのスクリプト配置用)
  • S3バケット(Glueジョブの一時ディレクトリ用)
  • S3バケット(GlueジョブのETL結果の格納先用)
  • Glueコネクションで使用するセキュリティグループ
  • Glueコネクション
  • Glueジョブ
  • 定期実行用Glueトリガー
  • ジョブ失敗通知用のイベントルール
  • SecurityConfigurationで使用するKMS
  • SecurityConfiguration
    • ※後述しますが、CloudFormationで作成できなかったため、AWS CLIで作成します


記事では省略する事前準備

  • VPC・サブネットの構築
  • MySQL自体の構築
  • MySQLユーザの作成
  • Secret ManagerでMySQLユーザのSecretの作成
    • Glue Connectionにパスワードなどを直書きしないためにSecret Managerを使用して構築します
  • Glueジョブの実装・ファイル


方法

ディレクトリ構成

.
├── glue-job-script
│   └── job_data_transfer.py
├── shdir
│   └── glue.sh
└── yamldir
    └── glue.yaml

CloudFormationテンプレートファイル(yaml)とシェルスクリプトファイルのみ記載します。

GitHubにはジョブファイルも載せています。(DynamoDB->MySQLのGlueジョブ)

github.com


CloudFormationテンプレートファイル(yml)

yamldir/glue.yaml

AWSTemplateFormatVersion: "2010-09-09"

Description: glue cfn

Metadata:
  "AWS::CloudFormation::Interface":
    ParameterGroups:
      - Label:
          default: "AppName"
        Parameters:
          - AppName
      - Label:
          default: "ConnectionName"
        Parameters:
          - ConnectionName
      - Label:
          default: "JDBCString"
        Parameters:
          - JDBCString
      - Label:
          default: "Secret"
        Parameters:
          - Secret
      - Label:
          default: "GlueScriptsBucket"
        Parameters:
          - GlueScriptsBucket
      - Label:
          default: "GlueResultsBucket"
        Parameters:
          - GlueResultsBucket
      - Label:
          default: "GlueTempBucket"
        Parameters:
          - GlueTempBucket
      - Label:
          default: "GlueScriptsFilename"
        Parameters:
          - GlueScriptsFilename
      - Label:
          default: "GlueSecurityGroupVpcID"
        Parameters:
          - GlueSecurityGroupVpcID
      - Label:
          default: "GlueConnectionSubnetID"
        Parameters:
          - GlueConnectionSubnetID
      - Label:
          default: "GlueConnectionSubnetAZName"
        Parameters:
          - GlueConnectionSubnetAZName
      - Label:
          default: "SnsTopicArn"
        Parameters:
          - SnsTopicArn
      - Label:
          default: "SnsTopicName"
        Parameters:
          - SnsTopicName

# ------------------------------------------------------------#
# Input Parameters
# ------------------------------------------------------------#
Parameters:
  AppName:
    Type: String
    Default: ""

  GlueServiceRolePolicyARN:
    Type: String
    Default: "arn:aws:iam::aws:policy/service-role/AWSGlueServiceRole"

  AmazonS3FullAccessPolicyARN:
    Type: String
    Default: "arn:aws:iam::aws:policy/AmazonS3FullAccess"

  ConnectionName:
    Type: String
    #Default: ""

  JDBCString:
    Type: String
    #Default: ""

  Secret:
    Type: String
    #Default: ""

  GlueScriptsBucket:
    Type: String
    #Default: ""

  GlueResultsBucket:
    Type: String
    #Default: ""

  GlueTempBucket:
    Type: String
    #Default: ""

  GlueScriptsFilename:
    Type: String
    #Default: ""

  GlueSecurityGroupVpcID:
    Type: String
    #Default: ""

  GlueConnectionSubnetID:
    Type: String
    #Default: ""

  GlueConnectionSubnetAZName:
    Type: String
    #Default: ""

  SnsTopicArn:
    Type: String
    #Default: ""

  SnsTopicName:
    Type: String
    #Default: ""

# ------------------------------------------------------------#
#  Resources
# ------------------------------------------------------------#
Resources:
  GlueRole:
    Type: "AWS::IAM::Role"
    Properties:
      RoleName: !Sub "${AppName}-GlueServiceRole"
      Description: "for Glue"
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - "glue.amazonaws.com"
            Action:
              - "sts:AssumeRole"
      Path: "/"
      ManagedPolicyArns:
        - !Ref GlueServiceRolePolicyARN
        - !Ref AmazonS3FullAccessPolicyARN

  GluePolicy:
    Type: "AWS::IAM::Policy"
    Properties:
      PolicyName: !Sub "${AppName}-GluePolicy"
      Roles:
        - !Ref GlueRole
      PolicyDocument:
        Statement:
          - Effect: Allow
            Action:
              - "logs:AssociateKmsKey"
            Resource: "arn:aws:logs:*:*:*"

  GlueScriptsS3Bucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Ref GlueScriptsBucket
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: AES256

  GlueResultsS3Bucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Ref GlueResultsBucket
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: AES256

  GlueTempS3Bucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Ref GlueTempBucket
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: AES256

  GlueSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties: 
      VpcId: !Ref GlueSecurityGroupVpcID
      GroupName: !Sub "${AppName}-Glue-sg"
      GroupDescription: "Glue Sg"
      Tags:
        - Key: "Name"
          Value: !Sub "Glue-sg"

  SelfRefSecurityGroupIgress:
    Type: AWS::EC2::SecurityGroupIngress
    Properties:
      GroupId: !GetAtt GlueSecurityGroup.GroupId
      IpProtocol: tcp
      FromPort: 0
      ToPort: 65535
      SourceSecurityGroupId: !GetAtt GlueSecurityGroup.GroupId

  GlueConnection:
    Type: AWS::Glue::Connection
    Properties:
      CatalogId: !Ref AWS::AccountId
      ConnectionInput: 
        Description: "Connect to MySQL database."
        ConnectionType: "JDBC"
        PhysicalConnectionRequirements:
          SecurityGroupIdList: 
            - !Ref GlueSecurityGroup
          SubnetId: !Ref GlueConnectionSubnetID
          AvailabilityZone: !Ref GlueConnectionSubnetAZName
        ConnectionProperties: {
          "JDBC_CONNECTION_URL": !Ref JDBCString,
          "USERNAME": !Sub '{{resolve:secretsmanager:${Secret}:SecretString:username}}',
          "PASSWORD": !Sub '{{resolve:secretsmanager:${Secret}:SecretString:password}}'
        }
        Name: !Sub "${ConnectionName}"

  GlueJob:
    Type: AWS::Glue::Job
    DependsOn:
      - GlueScriptsS3Bucket
      - GlueResultsS3Bucket
      - GlueTempS3Bucket
    Properties:
      Name: !Sub "${AppName}-job"
      Connections:
        Connections:
          - !Sub "${ConnectionName}"
      Role: !Ref GlueRole
      GlueVersion: "2.0"
      Command:
        Name: "glueetl"
        PythonVersion: "3"
        ScriptLocation: !Sub "s3://${GlueScriptsBucket}/${GlueScriptsFilename}"
      DefaultArguments:
        --JOB_NAME: !Sub "${AppName}-job"
        --connection_name: !Sub "${ConnectionName}"
        --bucket_root: !Sub "s3://${GlueResultsBucket}"
        --TempDir: !Sub "s3://${GlueTempBucket}"
        --enable-metrics: ""
        --enable-continuous-cloudwatch-log: "true"
        --enable-continuous-log-filter: "true"
        --continuous-log-logGroup: !Sub "/aws-glue/jobs/${AppName}"
      ExecutionProperty:
        MaxConcurrentRuns: 1
      WorkerType: "G.1X"
      NumberOfWorkers: 10
      MaxRetries: 1
      Timeout: 2880
      SecurityConfiguration: !Sub "${AppName}-security-config"
      Tags:
        Key: Name
        Value: !Sub "${AppName}-job"

  GlueJobTrigger:
    Type: AWS::Glue::Trigger
    Properties:
      Name: !Sub "${AppName}-trigger"
      Schedule: cron(0 16 * * ? *)
      Type: SCHEDULED
      Actions:
        - JobName: !Ref GlueJob
      StartOnCreation: true

  GlueJobFailureEventRule:
    Type: AWS::Events::Rule
    Properties:
      EventPattern:
        source:
          - aws.glue
        detail-type:
          - Glue Job State Change
        detail:
          state:
            - FAILED
          jobName:
            - !Ref GlueJob
      State: ENABLED
      Targets:
        - Arn: !Ref SnsTopicArn
          Id: !Ref SnsTopicName

  GlueLogsEncryptionKey:
    DeletionPolicy: Retain
    Type: AWS::KMS::Key
    Properties:
      Description: "CMK for Glue Job Logs Encryprioin etc"
      EnableKeyRotation: true
      Tags:
        - Key: "Name"
          Value: !Sub "GlueLogs-${AppName}-key"
      KeyPolicy:
        Version: "2012-10-17"
        Id: !Sub "GlueLogs-${AppName}-key"
        Statement:
          - Effect: "Allow"
            Principal:
              AWS: !Join
                - ""
                - - "arn:aws:iam::"
                  - !Ref "AWS::AccountId"
                  - ":root"
            Action: "kms:*"
            Resource: "*"
          - Effect: "Allow"
            Principal:
              Service: !Sub "logs.${AWS::Region}.amazonaws.com"
              AWS: !GetAtt GlueRole.Arn
            Action:
              - "kms:Encrypt*"
              - "kms:Decrypt*"
              - "kms:ReEncrypt*"
              - "kms:GenerateDataKey*"
              - "kms:Describe*"
            Resource: "*"

  GlueLogsEncryptionKeyAlias:
    Type: "AWS::KMS::Alias"
    Properties:
      AliasName: !Sub "alias/GlueLogs-${AppName}-key"
      TargetKeyId: !Ref GlueLogsEncryptionKey

  #### Internal Failureになってエラーになるので、CFn実行後にCLIで作成
  #GlueJobSecurityConfiguration:
  #  Type: AWS::Glue::SecurityConfiguration
  #  Properties:
  #    Name: !Sub "${AppName}-security-config"
  #    EncryptionConfiguration:
  #      CloudWatchEncryption:
  #        #CloudWatchEncryptionMode: "SSE-KMS"
  #        KmsKeyArn: !GetAtt GlueLogsEncryptionKey.Arn

# ------------------------------------------------------------#
# Output Parameters
# ------------------------------------------------------------#
Outputs:
  GlueLogsEncryptionKeyArn:
    Value: !GetAtt GlueLogsEncryptionKey.Arn
    Export:
      Name: !Sub "GlueLogsEncryptionKey-ARN"

解説

まず、Glueジョブで使用するIAMロール・IAMポリシーです。

GlueRoleにはとりあえずAWSGlueServiceRole、AmazonS3FullAccessという管理ポリシーをアタッチしました。

さらに追加でGluePolicyをアタッチしていて、これは後述するセキュリティ設定(SecurityConfiguration)でCloudWatch LogをKMSで暗号化するために必要なアクションになります。

"logs:AssociateKmsKey"Resource: "arn:aws:logs:*:*:*"部分は、実際は適切に絞るべきです。

  GlueRole:
    Type: "AWS::IAM::Role"
    Properties:
      RoleName: !Sub "${AppName}-GlueServiceRole"
      Description: "for Glue"
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - "glue.amazonaws.com"
            Action:
              - "sts:AssumeRole"
      Path: "/"
      ManagedPolicyArns:
        - !Ref GlueServiceRolePolicyARN
        - !Ref AmazonS3FullAccessPolicyARN

  GluePolicy:
    Type: "AWS::IAM::Policy"
    Properties:
      PolicyName: !Sub "${AppName}-GluePolicy"
      Roles:
        - !Ref GlueRole
      PolicyDocument:
        Statement:
          - Effect: Allow
            Action:
              - "logs:AssociateKmsKey"
            Resource: "arn:aws:logs:*:*:*"


次は以下のS3バケットを作成します。細かい設定は適宜変更してください。

  GlueScriptsS3Bucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Ref GlueScriptsBucket
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: AES256

  GlueResultsS3Bucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Ref GlueResultsBucket
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: AES256

  GlueTempS3Bucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Ref GlueTempBucket
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: AES256


次は、Glueコネクションで使用するセキュリティグループです。VPC IDはシェルからパラメータとして渡します。

具体的な設定は公式ドキュメントで詳細が記載されています。

docs.aws.amazon.com

  GlueSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties: 
      VpcId: !Ref GlueSecurityGroupVpcID
      GroupName: !Sub "${AppName}-Glue-sg"
      GroupDescription: "Glue Sg"
      Tags:
        - Key: "Name"
          Value: !Sub "Glue-sg"

  SelfRefSecurityGroupIgress:
    Type: AWS::EC2::SecurityGroupIngress
    Properties:
      GroupId: !GetAtt GlueSecurityGroup.GroupId
      IpProtocol: tcp
      FromPort: 0
      ToPort: 65535
      SourceSecurityGroupId: !GetAtt GlueSecurityGroup.GroupId


ここで重要な、MySQLと接続をするためのGlueコネクションを定義します。

MySQLユーザとそのシークレットは作成済みが前提ですので、そのシークレット名をシェルからパラメータで渡してこちらで指定します。

サブネットも作成済みが前提で、同じくシェルから渡します。

セキュリティグループは先ほど作成したものを指定します。

  GlueConnection:
    Type: AWS::Glue::Connection
    Properties:
      CatalogId: !Ref AWS::AccountId
      ConnectionInput: 
        Description: "Connect to MySQL database."
        ConnectionType: "JDBC"
        PhysicalConnectionRequirements:
          SecurityGroupIdList: 
            - !Ref GlueSecurityGroup
          SubnetId: !Ref GlueConnectionSubnetID
          AvailabilityZone: !Ref GlueConnectionSubnetAZName
        ConnectionProperties: {
          "JDBC_CONNECTION_URL": !Ref JDBCString,
          "USERNAME": !Sub '{{resolve:secretsmanager:${Secret}:SecretString:username}}',
          "PASSWORD": !Sub '{{resolve:secretsmanager:${Secret}:SecretString:password}}'
        }
        Name: !Sub "${ConnectionName}"


その次も同じく重要な、Glueジョブです。

色々と設定項目が多いですが、ワーカー・リトライ・タイムアウトの設定などは適宜調整をしてください。

  GlueJob:
    Type: AWS::Glue::Job
    DependsOn:
      - GlueScriptsS3Bucket
      - GlueResultsS3Bucket
      - GlueTempS3Bucket
    Properties:
      Name: !Sub "${AppName}-job"
      Connections:
        Connections:
          - !Sub "${ConnectionName}"
      Role: !Ref GlueRole
      GlueVersion: "2.0"
      Command:
        Name: "glueetl"
        PythonVersion: "3"
        ScriptLocation: !Sub "s3://${GlueScriptsBucket}/${GlueScriptsFilename}"
      DefaultArguments:
        --JOB_NAME: !Sub "${AppName}-job"
        --connection_name: !Sub "${ConnectionName}"
        --bucket_root: !Sub "s3://${GlueResultsBucket}"
        --TempDir: !Sub "s3://${GlueTempBucket}"
        --enable-metrics: ""
        --enable-continuous-cloudwatch-log: "true"
        --enable-continuous-log-filter: "true"
        --continuous-log-logGroup: !Sub "/aws-glue/jobs/${AppName}"
      ExecutionProperty:
        MaxConcurrentRuns: 1
      WorkerType: "G.1X"
      NumberOfWorkers: 10
      MaxRetries: 1
      Timeout: 2880
      SecurityConfiguration: !Sub "${AppName}-security-config"
      Tags:
        Key: Name
        Value: !Sub "${AppName}-job"

大事なのがDefaultArgumentsで、環境変数のような使い方をする、ジョブ内のコードで使用する各パラメータを指定します。キーにはすべて--を付けないといけないところは注意です。

      DefaultArguments:
        --JOB_NAME: !Sub "${AppName}-job"
        --connection_name: !Sub "${ConnectionName}"
        --bucket_root: !Sub "s3://${GlueResultsBucket}"
        --TempDir: !Sub "s3://${GlueTempBucket}"
        --enable-metrics: ""
        --enable-continuous-cloudwatch-log: "true"
        --enable-continuous-log-filter: "true"
        --continuous-log-logGroup: !Sub "/aws-glue/jobs/${AppName}"

ここで補足ですが、--TempDir--enable-metrics--enable-continuous-cloudwatch-log--enable-continuous-log-filter--continuous-log-logGroupはジョブ内のコードで使用するパラメータではなく、Glueジョブの詳細設定に必要な予約パラメータになります。

Argument 説明
--TempDir s3://~~~ ジョブ内のSparkの一部の処理で使用する一時ディレクトリとしてのS3バケット・キー
--enable-metrics ""(空文字) ジョブに関するメトリクスの収集
--enable-continuous-cloudwatch-log true リアルタイムの連続ログ記録
--enable-continuous-log-filter true 標準フィルタ(Apache Spark ドライバー/エグゼキュータや Apache Hadoop YARN ハートビートのログなどを除外する)
--continuous-log-logGroup CloudWatch Logグループ名 連続ロギングが有効なジョブのカスタム Amazon CloudWatch ロググループ名

詳細はこちらをご覧ください。

docs.aws.amazon.com


また、ScriptLocationはジョブファイルが置かれるS3のパス(バケット+キー)になります。シェルから渡すパラメータで表現しています。

        ScriptLocation: !Sub "s3://${GlueScriptsBucket}/${GlueScriptsFilename}"

SecurityConfigurationはこの後作成するものですが、その名前を指定します。

      SecurityConfiguration: !Sub "${AppName}-security-config"


次は、定期実行用Glueトリガーです。Type: SCHEDULEDを前提に説明します。

実行される時刻設定は、cron形式で記入します。(cron(0 16 * * ? *))

また、StartOnCreation: trueにしないとトリガーは有効化されず開始されません。

  GlueJobTrigger:
    Type: AWS::Glue::Trigger
    Properties:
      Name: !Sub "${AppName}-trigger"
      Schedule: cron(0 16 * * ? *)
      Type: SCHEDULED
      Actions:
        - JobName: !Ref GlueJob
      StartOnCreation: true


次は、ジョブ失敗通知用のイベントルールです。

ここでは上記ジョブが失敗したことをトリガーとしてSNSに通知する設定にしています。

  GlueJobFailureEventRule:
    Type: AWS::Events::Rule
    Properties:
      EventPattern:
        source:
          - aws.glue
        detail-type:
          - Glue Job State Change
        detail:
          state:
            - FAILED
          jobName:
            - !Ref GlueJob
      State: ENABLED
      Targets:
        - Arn: !Ref SnsTopicArn
          Id: !Ref SnsTopicName


ここで、後述するSecurityConfigurationで使用する、ログ暗号化に使用するKMSです。本来はActionをさらに絞った方がセキュアになりますがここではスコープ外なので触れません。

  GlueLogsEncryptionKey:
    DeletionPolicy: Retain
    Type: AWS::KMS::Key
    Properties:
      Description: "CMK for Glue Job Logs Encryprioin etc"
      EnableKeyRotation: true
      Tags:
        - Key: "Name"
          Value: !Sub "GlueLogs-${AppName}-key"
      KeyPolicy:
        Version: "2012-10-17"
        Id: !Sub "GlueLogs-${AppName}-key"
        Statement:
          - Effect: "Allow"
            Principal:
              AWS: !Join
                - ""
                - - "arn:aws:iam::"
                  - !Ref "AWS::AccountId"
                  - ":root"
            Action: "kms:*"
            Resource: "*"
          - Effect: "Allow"
            Principal:
              Service: !Sub "logs.${AWS::Region}.amazonaws.com"
              AWS: !GetAtt GlueRole.Arn
            Action:
              - "kms:Encrypt*"
              - "kms:Decrypt*"
              - "kms:ReEncrypt*"
              - "kms:GenerateDataKey*"
              - "kms:Describe*"
            Resource: "*"

  GlueLogsEncryptionKeyAlias:
    Type: "AWS::KMS::Alias"
    Properties:
      AliasName: !Sub "alias/GlueLogs-${AppName}-key"
      TargetKeyId: !Ref GlueLogsEncryptionKey

Statementの2つ目の、PrincipalService: !Sub "logs.${AWS::Region}.amazonaws.com"AWS: !GetAtt GlueRole.Arnになっているのは、SecurityConfigurationで暗号化するCloudWatch LogsとジョブのIAMロールがこのKMSにアクセスできるようにするためです。

詳細はリファレンスにあります。

docs.aws.amazon.com


次は、色々と途中で出てきたSecurityConfigurationです。これはログを暗号化するための設定です。

しかしコメントアウトしています。

  #### Internal Failureになってエラーになるので、CFn実行後にCLIで作成
  #GlueJobSecurityConfiguration:
  #  Type: AWS::Glue::SecurityConfiguration
  #  Properties:
  #    Name: !Sub "${AppName}-security-config"
  #    EncryptionConfiguration:
  #      CloudWatchEncryption:
  #        #CloudWatchEncryptionMode: "SSE-KMS"
  #        KmsKeyArn: !GetAtt GlueLogsEncryptionKey.Arn


実はコメントアウトしている理由は、これを上記コードでデプロイしたら、GlueJobSecurityConfigurationの作成がInternal Failureとなってスタック作成に失敗したからです。


この理由がわからず、代わりにシェル内でAWS CLIでやってみたところ成功したので、そのままにしています。

VPC内にKMSのVPCエンドポイントを作成しないといけないのかな?とも思ったけれど、それだとなぜCLIでは成功するのか・・・

あまり調査は出来ていないので、時間があれば調査してみたいと思いますがとりあえず今は暫定でCLIでやります。


最後に、OutputsでKMSキーのARNを出力しています。

これは、上記のSecurityConfigurationの作成のためにKMSのARNが必要なので、CLIでSecurityConfigurationを作成する際に取得できるようにこちらで出力します。

Outputs:
  GlueLogsEncryptionKeyArn:
    Value: !GetAtt GlueLogsEncryptionKey.Arn
    Export:
      Name: !Sub "GlueLogsEncryptionKey-ARN"


シェルスクリプト

shdir/glue.sh

#!/bin/bash
set -eu

cd $(dirname $0) && cd ../

CFN_TEMPLATE="./yamldir/glue.yaml"
GLUE_SCRIPTS_DIR="glue-job-script"
SCRIPT_NAME="job_data_transfer.py"
GlueScriptsFilename="${GLUE_SCRIPTS_DIR}/${SCRIPT_NAME}"

APP_NAME="gluetest"
CFN_STACK_NAME="${APP_NAME}"

CFN_REGION="ap-northeast-1"
AWSAccountID=$(aws sts get-caller-identity | jq -r '.Account')

GlueScriptsBucket="${APP_NAME}-${AWSAccountID}-scripts"
GlueResultsBucket="${APP_NAME}-${AWSAccountID}-results"
GlueTempBucket="${APP_NAME}-${AWSAccountID}-temp"


## "(〇〇)"の部分は実際の環境に合わせて埋めてください
ClusterEndpoint="(RDSのDBクラスターのエンドポイント, EC2ではホスト)"
ConnectionSecret="(Glueコネクションで使用するMySQLユーザのシークレット名)"
GlueSecurityGroupVpcID="(Glueで使用するセキュリティグループのVPCのID)"
GlueConnectionSubnetID="(Glue Connectionで使用するサブネットのID)"
GlueConnectionSubnetAZName="(Glue Connectionで使用するサブネットのAZ名 例:ap-northeast-1)"
SnsTopicName="(ジョブ実行失敗を通知するSNSトピック名)"
SnsTopicArn="(ジョブ実行失敗を通知するSNSトピックARN)"


JDBCString="jdbc:mysql://${ClusterEndpoint}:3306/information_schema"
ConnectionName="${APP_NAME}"

aws cloudformation deploy \
  --stack-name ${CFN_STACK_NAME} \
  --template-file ${CFN_TEMPLATE} \
  --capabilities CAPABILITY_IAM CAPABILITY_NAMED_IAM \
  --parameter-overrides \
  AppName=${APP_NAME} \
  ConnectionName=${ConnectionName} \
  JDBCString=${JDBCString} \
  Secret=${ConnectionSecret} \
  GlueScriptsBucket=${GlueScriptsBucket} \
  GlueResultsBucket=${GlueResultsBucket} \
  GlueTempBucket=${GlueTempBucket} \
  GlueScriptsFilename=${GlueScriptsFilename} \
  GlueSecurityGroupVpcID=${GlueSecurityGroupVpcID} \
  GlueConnectionSubnetID=${GlueConnectionSubnetID} \
  GlueConnectionSubnetAZName=${GlueConnectionSubnetAZName} \
  SnsTopicArn=${SnsTopicArn} \
  SnsTopicName=${SnsTopicName} \
  --no-fail-on-empty-changeset


### AWS::Glue::SecurityConfigurationがInternal Failureになってエラーになるので、CFn実行後にCLIで作成
SecurityConfigurationName="${APP_NAME}-security-config"

SecurityExists=`aws glue get-security-configurations \
    | jq '.SecurityConfigurations[] | select(.Name == "'${SecurityConfigurationName}'") | .Name' \
    | tr -d '"'`

if [ -n "${SecurityExists}" ]; then
    aws glue delete-security-configuration \
        --name ${SecurityConfigurationName} \
        > /dev/null
fi

KmsKeyName="GlueLogsEncryptionKey-ARN"
KmsKeyArn=`aws cloudformation describe-stacks \
    --stack-name ${CFN_STACK_NAME} \
    | jq '.Stacks[].Outputs[] | select(.ExportName == "'${KmsKeyName}'") | .OutputValue' \
    | tr -d '"'`

aws glue create-security-configuration \
    --name ${SecurityConfigurationName} \
    --encryption-configuration "CloudWatchEncryption={CloudWatchEncryptionMode=SSE-KMS,KmsKeyArn=${KmsKeyArn}}" \
    > /dev/null

### ジョブのコードのデプロイ
aws s3 rm s3://${GlueScriptsBucket} --recursive > /dev/null
aws s3 cp ${GLUE_SCRIPTS_DIR} s3://${GlueScriptsBucket}/${GLUE_SCRIPTS_DIR}/ --recursive > /dev/null

解説

GLUE_SCRIPTS_DIRSCRIPT_NAMEはそれぞれ最初の方に示したディレクトリ構成に合わせたジョブファイルのディレクトリ・ファイル名です。

GlueScriptsFilenameはS3に置くジョブのコードのパスで、手元の構成に合わせています。

GLUE_SCRIPTS_DIR="glue-job-script"
SCRIPT_NAME="job_data_transfer.py"
GlueScriptsFilename="${GLUE_SCRIPTS_DIR}/${SCRIPT_NAME}"


次はS3バケット名ですが、グローバルでユニークでないといけないので、AWSアカウントIDを入れています。リージョンごとにGlueを構築する場合は、バケット名にリージョンなどの情報も入れましょう。ただし、バケット名長の上限( 3~63 文字)にはご注意を。

またjqコマンドを使用しているので、インストールしておく必要があります。

AWSAccountID=$(aws sts get-caller-identity | jq -r '.Account')

GlueScriptsBucket="${APP_NAME}-${AWSAccountID}-scripts"
GlueResultsBucket="${APP_NAME}-${AWSAccountID}-results"
GlueTempBucket="${APP_NAME}-${AWSAccountID}-temp"


以下は事前構築前提のものですので、それぞれリソースを用意して埋めてください。

## "(〇〇)"の部分は実際の環境に合わせて埋めてください
ClusterEndpoint="(RDSのDBクラスターのエンドポイント, EC2ではホスト)"
ConnectionSecret="(Glueコネクションで使用するMySQLユーザのシークレット名)"
GlueSecurityGroupVpcID="(Glueで使用するセキュリティグループのVPCのID)"
GlueConnectionSubnetID="(Glue Connectionで使用するサブネットのID)"
GlueConnectionSubnetAZName="(Glue Connectionで使用するサブネットのAZ名 例:ap-northeast-1)"
SnsTopicName="(ジョブ実行失敗を通知するSNSトピック名)"
SnsTopicArn="(ジョブ実行失敗を通知するSNSトピックARN)"


JDBCStringはDBのエンドポイントをもとに構成します。

MySQLのポートを3306から変更している場合は、こちらも合わせて変更が必要です。

スラッシュ以降にinformation_schemaと書いてますがこれはデフォルトDBです。ジョブ内でコネクションの情報に指定している場合は、こちらでは何でもOKです。

JDBCString="jdbc:mysql://${ClusterEndpoint}:3306/information_schema"


ここで、SecurityConfigurationをAWS CLI作成します。

まず、ymlファイルのGlueジョブ部分で指定した名前を定義します。

### AWS::Glue::SecurityConfigurationがInternal Failureになってエラーになるので、CFn実行後にCLIで作成
SecurityConfigurationName="${APP_NAME}-security-config"

すでに存在していたら作り直す様に、削除します。(ここの作りは少し無理やりなので、アレンジ等お任せします。)

SecurityExists=`aws glue get-security-configurations \
    | jq '.SecurityConfigurations[] | select(.Name == "'${SecurityConfigurationName}'") | .Name' \
    | tr -d '"'`

if [ -n "${SecurityExists}" ]; then
    aws glue delete-security-configuration \
        --name ${SecurityConfigurationName} \
        > /dev/null
fi

Outputsで出力したKMSのARNをCloudFormationスタックから取得します。

KmsKeyName="GlueLogsEncryptionKey-ARN"
KmsKeyArn=`aws cloudformation describe-stacks \
    --stack-name ${CFN_STACK_NAME} \
    | jq '.Stacks[].Outputs[] | select(.ExportName == "'${KmsKeyName}'") | .OutputValue' \
    | tr -d '"'`


そして、SecurityConfigurationを作成します。

aws glue create-security-configuration \
    --name ${SecurityConfigurationName} \
    --encryption-configuration "CloudWatchEncryption={CloudWatchEncryptionMode=SSE-KMS,KmsKeyArn=${KmsKeyArn}}" \
    > /dev/null


最後に、Glueジョブのコードファイルを、S3スクリプトバケットにアップロードをします。

### ジョブのコードのデプロイ
aws s3 rm s3://${GlueScriptsBucket} --recursive > /dev/null
aws s3 cp ${GLUE_SCRIPTS_DIR} s3://${GlueScriptsBucket}/${GLUE_SCRIPTS_DIR}/ --recursive > /dev/null


実行

上記シェルを実行すると、Glueのデプロイが開始します。

sh shdir/glue.sh


最後に

非常に長くなりました。しかし全部書きたかった・・・!

部分部分でもし参考にしていただけたら幸いです(そもそも読んでもらえただけで嬉しいです)。

途中のSecurityConfigurationのInternal Failureエラー問題はどこかで調査・対処できたらなと思っています。