データ分析基盤を構築しよう

このハンズオンでは、Amazon RDSのデータをGlue・Athena・QuickSightを使って分析・可視化するデータ分析基盤を実際にハンズオン形式で手を動かしながら体験します。

  • テスト用データベース(VPC・RDS・踏み台サーバ)の構築
  • AWS Glueによるデータの抽出・変換・ロード(ETL)
  • Amazon Athenaによるデータの分析
  • Amazon QuickSightによるデータの可視化

1. 事前準備

この講座のハンズオンでは、以下のツールやアカウントが必要です。まだ準備できていない場合は、リンク先の手順に沿って準備をお願いします。

2. ハンズオンの概要

このハンズオンでは、Amazon RDSにあるデータをAWS Glueで抽出・変換し、Amazon S3にParquet形式で格納する、データ分析基盤としてもっともオーソドックスな方法の一つを扱います。

RDSに直接SQLを発行することで分析は可能ですが、特に本番環境においてそれは適切ではありません。分析用のSQLは複雑になるケースも多く、データベースに負荷をかけ、本番システムに影響を与える可能性があります。また、通常はSELECTのみ許可する設定にしますが、設定ミスによって不用意なアップデートが行われるリスクも存在します。

このような理由から、本番のRDSを直接参照せず、分析に特化した環境を別途構築する必要があります。

このハンズオンでは、まずCloudFormationを使ってVPC・RDS・踏み台サーバを構築し、テストデータを登録します。次に、AWS Glueを使ってRDSからデータを抽出し、Parquet形式でS3に保存します。続いて、AthenaでS3上のデータに対してSQLクエリを実行し、最後にQuickSightを使って分析結果をグラフや表で可視化します。

今回構築するデータ分析基盤の全体構成を示します。

graph LR
    A["Amazon RDS<br>(データソース)"] -->|"データ抽出"| B["AWS Glue<br>(ETL処理)"]
    B -->|"Parquet形式で保存"| C["Amazon S3<br>(データレイク)"]
    C -->|"SQLクエリ"| D["Amazon Athena<br>(データ分析)"]
    D -->|"可視化"| E["Amazon QuickSight<br>(BI)"]

    style A fill:#e3f2fd,stroke:#1565c0
    style B fill:#fff3e0,stroke:#e65100
    style C fill:#e8f5e9,stroke:#2e7d32
    style D fill:#f3e5f5,stroke:#6a1b9a
    style E fill:#fce4ec,stroke:#c62828

3. テスト用データベースの作成

このセクションでは、分析対象のデータが格納されるRDSインスタンス、およびRDSを配置するためのVPC関連のリソースとして、下記内容を作成していきます。

リソース 名前 説明
VPC my-vpc 10.0.0.0/21のアドレス範囲を持つ仮想ネットワーク
サブネット db-subnet-01, db-subnet-02, api-subnet DB用サブネット2つ(10.0.0.0/24, 10.0.1.0/24)と踏み台サーバ用サブネット(10.0.2.0/24)
インターネットゲートウェイ my-internet-gateway 踏み台サーバへのインターネットアクセス用
ルートテーブル api-route-table api-subnetからインターネットへのルーティング
セキュリティグループ db-sg, jump-sg RDS用とEC2(踏み台サーバ)用
DBサブネットグループ my-subnet-group RDS用のサブネットグループ
RDSインスタンス testdb MySQLデータベース
EC2インスタンス JumpServer 踏み台サーバ

これらのリソースはデータ分析の本質ではないため、今回は説明を省略します。

また、リソースの作成についても簡略化するため、CloudFormationテンプレートにより、リソースを一括で作成します。

📝 CloudFormationとは
CloudFormationは、AWSのリソースをテンプレート(YAML/JSON)で定義し、自動的に作成・管理できるサービスです。手動でリソースを一つずつ作成する代わりに、テンプレートをアップロードするだけで必要なリソースをまとめて構築できます。
IaC(Infrastructure as Code)ツールとしてはTerraformも広く使われていますが、Terraformは事前にインストールが必要です。一方、CloudFormationはAWSマネジメントコンソールからそのまま実行できるため、今回のハンズオンではCloudFormationを使用します。
なお、このハンズオンではCloudFormationテンプレートの内容を理解する必要はありません。テンプレートをそのままアップロードして環境を構築してください。

また、環境構築が完了した後、踏み台サーバにログインし、MySQLクライアントのインストールとテストデータの登録を手動で行います。

3.1 CloudFormationによる環境構築

CloudFormationのダッシュボードを開き、「スタックの作成」>「新しいリソースを使用(標準)」をクリックします。

その後、画面に表示されるスタックの作成をクリックします。

スタックの作成の設定を行っていきます。下記表に従い、設定を行ってください。設定後、次へをクリックしてください。

設定項目 設定の基準
テンプレートの準備 既存のテンプレートを選択 事前に作成したテンプレートファイルを使用するため
テンプレートソース テンプレートファイルのアップロード ローカルのテンプレートファイルを直接アップロードするため
テンプレートファイルのアップロード 以下のYAMLテンプレートを保存したファイル ハンズオン環境を構築するテンプレートを指定するため

アップロードするのは下記ファイルです。こちらをtemplate.ymlのような任意の名前でローカル環境に保存し、それをアップロードしてください。

AWSTemplateFormatVersion: '2010-09-09'
Description: Data Analysis Hands-on Environment

Parameters:
  DBMasterPassword:
    Type: String
    NoEcho: true
    Description: Password for RDS master user (admin)
    MinLength: 8

  LatestAmiId:
    Type: AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>
    Default: /aws/service/ami-amazon-linux-latest/al2023-ami-kernel-6.1-x86_64

Resources:
  # ==================== VPC ====================
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 10.0.0.0/21
      EnableDnsSupport: true
      EnableDnsHostnames: true
      Tags:
        - Key: Name
          Value: my-vpc

  # ==================== Subnets ====================
  DBSubnet01:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: 10.0.0.0/24
      AvailabilityZone: ap-northeast-1a
      Tags:
        - Key: Name
          Value: db-subnet-01

  DBSubnet02:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: 10.0.1.0/24
      AvailabilityZone: ap-northeast-1c
      Tags:
        - Key: Name
          Value: db-subnet-02

  APISubnet:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: 10.0.2.0/24
      AvailabilityZone: ap-northeast-1a
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: api-subnet

  # ==================== Internet Gateway ====================
  InternetGateway:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Name
          Value: my-internet-gateway

  VPCGatewayAttachment:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: !Ref VPC
      InternetGatewayId: !Ref InternetGateway

  # ==================== Route Table ====================
  PublicRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: api-route-table

  PublicRoute:
    Type: AWS::EC2::Route
    DependsOn: VPCGatewayAttachment
    Properties:
      RouteTableId: !Ref PublicRouteTable
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref InternetGateway

  APISubnetRouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref APISubnet
      RouteTableId: !Ref PublicRouteTable

  # ==================== Security Groups ====================
  DBSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Security group for RDS
      GroupName: db-sg
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: db-sg

  JumpSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Security group for Jump Server
      GroupName: jump-sg
      VpcId: !Ref VPC
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 22
          ToPort: 22
          CidrIp: 0.0.0.0/0
      Tags:
        - Key: Name
          Value: jump-sg

  DBFromJumpIngress:
    Type: AWS::EC2::SecurityGroupIngress
    Properties:
      GroupId: !Ref DBSecurityGroup
      IpProtocol: tcp
      FromPort: 3306
      ToPort: 3306
      SourceSecurityGroupId: !Ref JumpSecurityGroup

  # ==================== DB Subnet Group ====================
  DBSubnetGroup:
    Type: AWS::RDS::DBSubnetGroup
    Properties:
      DBSubnetGroupName: my-subnet-group
      DBSubnetGroupDescription: my subnet group
      SubnetIds:
        - !Ref DBSubnet01
        - !Ref DBSubnet02

  # ==================== RDS ====================
  RDSInstance:
    Type: AWS::RDS::DBInstance
    Properties:
      DBInstanceIdentifier: testdb
      Engine: mysql
      DBInstanceClass: db.t3.micro
      AllocatedStorage: 20
      MasterUsername: admin
      MasterUserPassword: !Ref DBMasterPassword
      DBSubnetGroupName: !Ref DBSubnetGroup
      VPCSecurityGroups:
        - !Ref DBSecurityGroup
      PubliclyAccessible: false
      AvailabilityZone: ap-northeast-1a

  # ==================== EC2 (Jump Server) ====================
  JumpServer:
    Type: AWS::EC2::Instance
    Properties:
      InstanceType: t3.micro
      ImageId: !Ref LatestAmiId
      SubnetId: !Ref APISubnet
      SecurityGroupIds:
        - !Ref JumpSecurityGroup
      Tags:
        - Key: Name
          Value: JumpServer

Outputs:
  RDSEndpoint:
    Description: RDS Endpoint Address
    Value: !GetAtt RDSInstance.Endpoint.Address
  JumpServerInstanceId:
    Description: Jump Server Instance ID
    Value: !Ref JumpServer
  VPCId:
    Description: VPC ID
    Value: !Ref VPC

「次へ」をクリックし、スタックの詳細を設定します。

設定項目 設定の基準
スタック名 data-analysis-handson ハンズオン用のスタックであることを識別するため
DBMasterPassword 任意のパスワード RDSへの接続パスワード。後ほど使用するため控えておくこと
LatestAmiId デフォルト Amazon Linux 2023の最新AMIを自動取得するため

「次へ」をクリックし、スタックオプションはデフォルトのまま「次へ」をクリックします。

確認画面で内容を確認し、「送信」をクリックします。

そうするとCloudFormationのスタックが作成されます。ステータスがCREATE_IN_PROGRESSとなり、作成中のステータスであることがわかります。スタックの作成には10〜15分ほどかかります。

ステータスがCREATE_COMPLETEになれば、リソースの作成が完了です。

3.2 RDSへの接続先を確認

今回作成されたリソースの中から、データ分析を行う上で必要となる項目を確認しておきます。

スタックの詳細画面で「出力」タブをクリックしてください。以下の値が表示されます。

キー 説明 用途
RDSEndpoint RDSインスタンスのエンドポイントアドレス テストデータの登録やGlue Connectionの作成時に使用

また、RDSのコンソールから、testdbの詳細画面でも確認ができます。

また、RDSへの接続には以下の情報も必要です。これらはCloudFormationのスタック作成時に設定した値です。

項目 確認方法
ユーザ名 admin CloudFormationテンプレートで固定値として設定済み
パスワード スタック作成時に指定したDBMasterPassword CloudFormationの画面には表示されないため、設定時に控えた値を使用
ポート 3306 MySQLのデフォルトポート

これらの値は後のステップで繰り返し使用するため、テキストエディタなどに控えておいてください。

3.3 データの登録

続いて、作成したRDSインスタンスに、テストデータを登録していきます。RDSにアクセスするための踏み台サーバとなるEC2インスタンスを作成しているので、これを使ってRDSインスタンスに接続し、データを登録していきます。

EC2インスタンスにログイン

まずはEC2インスタンスから、先程CloudFormationで作成したJumpServerをチェックし、接続をクリックします。

その後、デフォルトの状態で構いませんので、接続をクリックします。

以下のように接続完了の画面が表示されれば、無事にEC2インスタンスにログインできています。

📝 EC2 Instance Connectとは
EC2 Instance Connectとは、AWSマネジメントコンソールのブラウザ上から直接EC2インスタンスにSSH接続できる機能です。ローカルPCにSSHクライアントをインストールしたり、秘密鍵ファイルを管理したりする必要がなく、ブラウザだけで簡単にサーバへアクセスできます。Amazon Linux 2023やUbuntuなど、対応するAMIで利用可能です。

3.4 MySQLクライアントのインストール

ログインができたら、MySQLクライアントをインストールします。以下のコマンドによりMySQLクライアントをインストールします。

sudo dnf install -y mariadb105

3.5 MySQLへの接続

インストールが終わったら、RDSに接続します。以下のコマンドで接続が可能です。RDSのエンドポイントは、先程メモした、CloudFormationで作成されたRDSのエンドポイントに置き換えます。

mysql -h RDSのエンドポイント -u admin -p

パスワードの入力を求められるので、こちらもCloudFormation実行時にパラメータとして指定した、RDSのパスワードを入力します。

MariaDB [(none)]>のような待受状態になれば、無事にRDSインスタンスに接続できています。

テストデータの登録

SQLを実行し、テストデータを登録していきます。

まずは、datalake_testというデータベースを作成します。

CREATE DATABASE datalake_test;

続いて、usersというテーブルを作ります。

CREATE TABLE datalake_test.users (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(255) NOT NULL,
    age INT,
    gender INT
);

さらに、usersに10件程度のレコードを追加します。

INSERT INTO datalake_test.users (name, age, gender) VALUES
('Yamada Taro',      28, 1),
('Suzuki Hanako',    25, 2),
('Sato Kenji',       34, 1),
('Tanaka Yuki',      22, 2),
('Kobayashi Shota',  31, 1),
('Watanabe Aiko',    29, 2),
('Ito Naoki',        40, 1),
('Nakamura Mika',    27, 2),
('Kato Kenta',       36, 1),
('Yamamoto Rina',    33, 2);

最後に、無事にデータが登録されているかを、SELECTにより確認します。

SELECT * FROM datalake_test.users;

以下のように10件のレコードが表示されれば、ここまでは成功です。

+----+-----------------+------+--------+
| id | name            | age  | gender |
+----+-----------------+------+--------+
|  1 | Yamada Taro     |   28 |      1 |
|  2 | Suzuki Hanako   |   25 |      2 |
|  3 | Sato Kenji      |   34 |      1 |
|  4 | Tanaka Yuki     |   22 |      2 |
|  5 | Kobayashi Shota |   31 |      1 |
|  6 | Watanabe Aiko   |   29 |      2 |
|  7 | Ito Naoki       |   40 |      1 |
|  8 | Nakamura Mika   |   27 |      2 |
|  9 | Kato Kenta      |   36 |      1 |
| 10 | Yamamoto Rina   |   33 |      2 |
+----+-----------------+------+--------+
10 rows in set (0.000 sec)

4. データの抽出と格納

このセクションでは、テスト用のRDSからデータを抽出し、S3に格納するまでの一連のETL処理を行います。具体的には、データを格納するS3バケット、Glueに権限を与えるIAMロール、Glue用のサブネット、S3に接続するためのエンドポイントなどの下準備を行った後、Glueの設定を行いETL処理を実行します。

4.1 S3バケットの作成

まずは、データを格納するためのS3バケットを事前に作成していきます。

バケットの作成

S3のダッシュボードから、バケットの作成をクリックします。

バケット名は任意ですが、今回はglue-data-<アカウントID>とします。<アカウントID>の部分はご自身のAWSアカウントIDに置き換えてください。これは、S3バケットは全世界で一意になる必要があるためです。

それ以外の項目は今回デフォルトで構いませんので、バケットを作成をクリックします。

無事にS3バケットが作成されればここまでの操作は完了です。

フォルダの作成

続いて、データを格納するためのフォルダを作成します。

先ほど作成したバケットをクリックします。

次に、フォルダの作成をクリックします。

フォルダ名にrawdataと入力し、「フォルダの作成」をクリックします。

無事にフォルダが作成されたら、続けてrawdataフォルダの中に入り、さらにusersフォルダを作成します。同じ手順で「フォルダの作成」をクリックし、フォルダ名にusersと入力して作成してください。

最終的に、以下のようなフォルダ構成になっていれば完了です。

s3://glue-data-(識別子)/
  └── rawdata/
        └── users/

💡 ポイント
rawdataは「生データ(未加工データ)」を意味します。データ分析基盤では、データソースから抽出したデータをそのままの形で保存する領域としてrawdataフォルダを設けるのが一般的です。生データを加工せずに保持しておくことで、後から別の変換ロジックを適用したり、加工ミスがあった場合に元データから再処理したりすることが可能になります。rawdataの下にはテーブルごとのフォルダ(usersordersなど)を作成し、データを分けて管理します。実務ではrawdataの他に、加工済みデータを格納するprocessedや、分析用に整形したデータを格納するcuratedなどのフォルダを用途ごとに分けて管理します。

4.2 IAMの作成

続いて、Glueに割り当てるためのIAMロールを作成します。GlueはETLジョブの実行時に、割り当てられたIAMロールの権限を使ってAWSリソースを操作します。今回はGlueがRDSからデータを読み取り、S3に書き込む必要があるため、これらのアクセスを許可するIAMポリシーを作成し、それをIAMロールに紐づけます。

IAMポリシーの作成

まずは、IAMロールに割り当てるためのIAMポリシーを作成していきます。

IAMのダッシュボードを開き、左側のメニューから「ポリシー」を選択し、「ポリシーの作成」をクリックします。

アクセス許可の指定では「JSON」を選択し、以下のJSONを貼り付けて「次へ」をクリックします。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "glue:*",
                "rds:DescribeDBInstances",
                "ec2:*",
                "s3:PutObject",
                "s3:PutObjectAcl",
                "s3:GetObject",
                "s3:ListBucket",
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": "*"
        }
    ]
}

このポリシーの内容を簡単に表にまとめています。

アクション 許可する内容 必要な理由
glue:* Glueの全操作 ジョブの実行やデータカタログの操作を行うため
rds:DescribeDBInstances RDSインスタンス情報の参照 接続先RDSのエンドポイントなどの情報を取得するため
ec2:* EC2関連リソースの全操作 GlueをVPC内で実行する際にENI(ネットワークインターフェース)を作成・管理するため
s3:PutObject / s3:PutObjectAcl S3へのオブジェクトの書き込み 抽出・変換したデータをS3に保存するため
s3:GetObject / s3:ListBucket S3からのオブジェクトの読み取り・一覧取得 S3上のデータやETLスクリプトを読み取るため
logs:CreateLogGroup / CreateLogStream / PutLogEvents CloudWatch Logsへのログ出力 Glueジョブの実行ログを記録・確認できるようにするため

ポリシー名にglue-test-policyと入力し、「ポリシーの作成」をクリックします。

IAMポリシーが作成されれば、ここまでの操作は完了です。

IAMロールの作成

続いて、Glueに設定するためのIAMロールを作成します。

IAMのダッシュボードにて、左側のメニューから「ロール」を選択し、「ロールを作成」をクリックします。

信頼されたエンティティタイプにはAWSのサービスを選択し、次へをクリックします。

許可ポリシーでは、先程作成したglue-test-policyを選択し、次へをクリックします。

ロール名にはglue-test-roleを入力し、ロールの作成をクリックします。

IAMロールが作成されれば、ここまでの操作は完了です。

4.3 Glue用サブネットの作成

続いて、Glueを配置するためのサブネットを作成します。今回はRDSからのデータ取得を行うため、GlueをVPC内のサブネットに配置する必要があります。

💡ポイント
Glueは、データソースがVPC内にある場合(RDS、Redshift、ElastiCacheなど)にのみサブネットへの配置が必要です。VPC内のリソースはインターネットから直接アクセスできないため、Glue自身も同じVPC内に配置して通信する必要があります。一方、S3やDynamoDBなどパブリックなエンドポイントを持つサービスのみを扱う場合は、サブネットへの配置は不要です。

VPCのダッシュボードを開き、左側のメニューから「サブネット」をクリックし、「サブネットを作成」をクリックします。

下記の内容を設定してください。

設定項目 設定の基準
VPC my-vpc RDSが配置されているVPCを指定するため
サブネット名 glue-subnet Glue用のサブネットであることを識別するため
アベイラビリティーゾーン ap-northeast-1a RDSと同じAZに配置するため
IPv4サブネットCIDRブロック 10.0.3.0/24 他のサブネットと重複しない範囲を指定するため

入力が完了したらサブネットを作成をクリックします。

サブネットが作成されれば、ここまでの操作は完了です。

4.4 Glue用セキュリティグループの作成

続いて、Glueに設定するためのセキュリティグループを作成します。GlueをVPC内のサブネットに配置する場合、セキュリティグループの割り当てが必須です。このセキュリティグループによって、Glueがどのリソースと通信できるかが制御されます。

セキュリティグループの作成

Glue用のセキュリティグループを作成します。

VPCのダッシュボードを開き、左側のメニューから「セキュリティグループ」を選択し、「セキュリティグループを作成」をクリックします。

下記の内容を設定してください。

設定項目 設定の基準
セキュリティグループ名 glue-sg Glue用であることを識別するため
VPC my-vpc 作成したVPC内のリソースに適用するため

なお、インバウンドルールは今回少し特殊な設定を行うため、一度セキュリティグループを作った後に再設定する必要があります。

入力が完了したら「セキュリティグループを作成」をクリックします。

セキュリティグループが作成されれば、ここまでの操作は完了です。

インバウンドルールの編集

続いて、インバウンドルールの設定を行います。

インバウンドルールの編集をクリックします。

まずは設定を行いながら、その設定の説明を行います。

ルールを追加をクリックし、タイプからすべてのトラフィックをクリックします。

💡 ポイント
Glueジョブは内部的にApache Sparkで動作しており、複数のワーカーノードがタスクの分散・シャッフル・集約などさまざまな通信を行います。これらの通信は動的にポートが割り当てられるため、特定のポートだけを許可することができません。そのため、同一セキュリティグループ内に限定した上で「すべてのトラフィック」を許可しています。ソースに自分自身のセキュリティグループ(glue-sg)を指定することで、外部からのアクセスは遮断しつつ、ワーカー間の通信のみを許可する安全な設定になっています。

続いてソースを設定します。ここでは自分自身のセキュリティグループ(glue-sg)を選択します。

💡 ポイント
ソースに自分自身のセキュリティグループを指定する設定を自己参照(セルフリファレンス) と呼びます。これは「同じセキュリティグループに属するリソース同士の通信を許可する」という意味です。Glueジョブは複数のワーカーノードが並列で起動しますが、すべてのワーカーが同じglue-sgに属しているため、この自己参照ルールによってワーカー同士が互いに通信できるようになります。

入力が完了したらルールを保存をクリックします。

無事にインバウンドルールの更新が完了したら、ここまでの操作は成功です。

4.5 RDSのセキュリティグループを更新

続いて、RDSのセキュリティグループを更新します。GlueからRDSに接続してデータを取得するためには、RDSのセキュリティグループに対してGlueのセキュリティグループからの通信を許可する必要があります。

VPCのダッシュボードにて、左側のメニューから「セキュリティグループ」を選択し、db-sgのセキュリティグループIDのリンクをクリックします。なお、このdb-sgは、先程CloudFormationで作ったセキュリティグループです。

続いて、インバウンドルールを編集をクリックします。

ルールを追加をクリックし、タイプにMySQL/Aurora、ソースにglue-sgを指定します。これにより、RDSに対して、Glueからデータの参照を行うことができるようになります。

入力が完了したらルールを保存をクリックします。

無事にインバウンドルールの更新が完了したら、ここまでの操作は成功です。

4.6 S3エンドポイントの作成

続いて、S3エンドポイントを作成します。GlueはPrivate Subnetに配置するため、インターネット経由でS3にアクセスすることはできません。そのため、VPCエンドポイント(Gateway型)を作成し、GlueからS3へアクセスできるようにします。

VPCのダッシュボードを開き、左側のメニューから「エンドポイント」をクリックし、「エンドポイントを作成」をクリックします。

下記の内容を設定してください。

設定項目 設定の基準
名前タグ glue-endpoint エンドポイントを識別するため
サービスカテゴリ AWSのサービス AWS提供のサービスに接続するため
サービス com.amazonaws.ap-northeast-1.s3(Gateway) S3にGateway型で接続するため
VPC my-vpc 作成したVPC内で使用するため
ルートテーブル glue-subnetが使用しているルートテーブル(VPCのメインルートテーブル) Glueサブネットからの通信がS3エンドポイントを経由するようにするため
ポリシー フルアクセス S3への読み書きを許可するため
💡 ポイント
glue-subnetにはカスタムルートテーブルを明示的に関連付けていないため、VPCのメインルートテーブルが使用されています。ルートテーブルの一覧でapi-route-tableではなく、名前が設定されていない方のルートテーブル(VPCのメインルートテーブル)を選択してください。誤ったルートテーブルを選択すると、GlueからS3へのテスト接続時に「Could not find S3 endpoint or NAT gateway...」というエラーが発生します。

入力が完了したらエンドポイントを作成をクリックします。

VPCエンドポイントが作成されれば、ここまでの操作は完了です。

4.7 Data Connectionの作成

続いて、GlueのConnectionを作成します。Connectionは、GlueからRDSなどの外部データベースに接続するための設定です。接続先のエンドポイントやポート、認証情報、利用するVPCのサブネットやセキュリティグループなどを指定します。

Connectionの作成

Glueのダッシュボードを開き、左側のメニューからData connectionsをクリックし、Create connectionをクリックします。

「Choose data source」でjdbcを選択し、Nextをクリックします。

「Configure connection」に下記の内容を設定してください。

設定項目 設定の基準
JDBC URL jdbc:mysql://(RDSのエンドポイント):3306/datalake_test RDSのdatalake_testデータベースに接続するため
Credential type Username and password ユーザ名とパスワードで認証するため
Username admin RDS作成時に指定したマスターユーザ名
Password (RDSのパスワード) RDS作成時に指定したパスワード
VPC my-vpc RDSが配置されているVPCを指定するため
Subnet glue-subnet Glue用に作成したサブネットを指定するため
Security groups glue-sg Glue用に作成したセキュリティグループを指定するため

nameはglue-connectionと入力し、次へをクリックします。

確認画面が表示されるため、問題なければcreate connectionをクリックします。

接続確認

続いて、作成したglue-connectionの動作確認を行っていきます。

glue-connectionをチェックし、ActionからTest Connectionを選択します。

ここで、Glueの実行で使用するIAM Roleの選択が求められます。ここでは、先程作成したglue-test-roleを選択し、Confirmをクリックします。

しばらく経過すると、以下のような接続成功の表示がされればOKです。

4.8 ETLジョブの作成と実行

それではいよいよ、GlueによるETL処理を作成していきます。RDSからデータを抽出(Extract)し、Parquet形式に変換(Transform)したうえで、S3に格納(Load)する一連の処理を設定します。

ETLジョブの作成

まずはETLジョブを作成する画面を開きます。

Glueのダッシュボードにて、左側のメニューからVirtual ETLを選択し、表示されるメニューからVirtual ETLを選択します。

入力(ソース)を設定

続いて、入力(ソース)を指定していきます。今回は、CloudFormationで作成したRDSインスタンスを使用します。

Add Nodeのボタン(+で表記されていると思います)を選択し、MySQLを選択します。ここで、MySQLにはSourceとTargetの二つが該当するので、間違いなくSourceの方を選んでください。Targetの方は、出力で使用するものなので、設定が異なります。

左側にプロパティメニューが表示されるので、下記の内容を設定してください。

設定項目 設定の基準
JDBC source JDBC connection details JDBC接続を使用するため
Connection name glue-connection 先ほど作成したConnectionを使用するため
Table name users テストデータを登録したテーブルを指定するため
IAM Role glue-test-role Glueに権限を付与するため

数分経過すると、テーブルの値がプレビューされます。

出力先(ターゲット)を設定

続いて、出力先となるS3バケットを指定していきます。

Add nodesからS3を選択します。このときも、SOURCEとTARGETで二つのS3があるため、TARGETの方を選択してください。

左側にプロパティメニューが表示されるので、各項目を設定していきます。

まず、Node parentsMySQLを選択します。これにより、先ほど設定した入力ノード(RDSのusersテーブル)からデータを受け取る構成になります。

次に、FormatParquetを選択します。Parquetは列指向のファイル形式で、圧縮効率が高く分析処理に適しているため、データレイクへの格納形式として広く使われています。

最後に、S3 Target Locationを設定します。ここで、先ほど作成したrawdata/usersフォルダを指定してください。パスはs3://glue-data-(識別子)/rawdata/users/の形式になります。このフォルダにRDSから抽出・変換されたデータがParquet形式で保存されます。

💡 ポイント
S3 Target Locationでは、必ずrawdata/users/フォルダを指定してください。rawdata/の下にテーブルごとのフォルダを分けておくことで、後からテーブルが増えた場合もクローラがrawdata/配下を一括スキャンするだけで、テーブルごとに自動でデータカタログを作成してくれます。

入力が終わったら、ジョブの名前にtest-jobと入力したうえで、Saveをクリックして変更内容を保存します。

無事にジョブが作成されれば、ここまでは成功です。

ETLの実行

続いて、実際にETL処理を実行していきます。

まず、Runsのタブをクリックし、そこに表示されるRunをクリックします。

実行が開始されると、Run StatusがRunningとなります。実行完了までしばらく待ちます。

実行が完了したら、Run statusがSucceededとなります。

最後に、先程作ったS3バケットのrawdata/usersフォルダを開くと、parquet拡張子のデータがいくつか作成されていることが確認できます。

ここまでで、Glueを用いてRDSからデータを抽出し、S3にParquet形式で格納する処理が完了しました。

5. データの分析

このセクションでは、S3バケットに保存されたParquet形式のデータをAthenaから参照できるようにします。Athena上でテーブルを作成し、S3上のデータに対してSQLクエリを実行できるようにするまでの手順を説明します。

5.1 データカタログの設定(クローラの作成と実行)

Athenaでテーブルを作成する方法はいくつかありますが、今回はGlueクローラを利用する方法で進めます。Glueクローラは、S3などのデータソースを自動的にスキャンし、データのスキーマ(カラム名やデータ型)を検出して、Glueデータカタログにテーブル定義を自動作成する機能です。

📝 Glueクローラとは
Glueクローラは、指定したデータソース(S3、RDSなど)を自動的にスキャンし、データの構造(スキーマ)を検出してGlueデータカタログにテーブル定義を作成・更新する機能です。手動でカラム名やデータ型を定義する必要がなく、データの形式を自動で判別してくれるため、大量のテーブルを扱う場合やスキーマの変更が頻繁な場合に特に有用です。

クローラの作成

Glueのダッシュボードを開き、左側のメニューから「Crawlers」を選択し、「Create crawler」をクリックします。

クローラ名にtest-crawlerと入力し、「Next」をクリックします。

データソースの設定

「Add a data source」をクリックし、下記の内容を設定してください。

設定項目 設定の基準
Data source S3 S3上のデータを読み取るため
S3 path s3://glue-data-(識別子)/rawdata/users/ ETL処理で出力したS3パスを指定するため

入力が終わったら、Add an S3 data sourceをクリックしてください。

入力が問題なければ、Nextをクリックしてください。

IAMロールの設定

Crawlerの実行で利用するIAMロールを設定します。今回は、事前に作成しているglue-test-roleを使用します。

選択が終わったらNextをクリックします。

出力先の設定

続いて、対象となるデータベースをクリックします。今回は、Athenaで最初から作られているdefaultデータベースを利用します。

Crawler scheduleについて、時刻起動など設定できますが、今回は手動で実行するOn demandを選択します。

入力が終わったらNextをクリックします。

確認画面が表示されるので、問題なければCreate crawlerをクリックします。

クローラの実行

作成されたCrawlerにて、Run crawlerボタンをクリックし、Crawlerを実行します。

数分経過すると、クローラの実行が完了し、StatusがCompletedになります。

テーブルの確認

左側のメニューから「Data Catalog」>「Tables」を開き、usersテーブルが自動作成されていることを確認します。テーブルをクリックすると、クローラが自動検出したスキーマ(カラム名・データ型)を確認できます。

5.2 Athenaでデータを参照

Athenaのダッシュボードを開きます。テーブル一覧の中に、先ほど作成したusersテーブルが表示されています。

右側の三点メニューから「テーブルをプレビュー」をクリックします。テーブルに保存されているデータが画面上に表示されることを確認できます。

以上で、Athenaを利用してS3に保存されたParquet形式のデータを参照する設定は完了です。

6. データの可視化

このセクションでは、Athenaで取得したデータをQuickSightで可視化します。QuickSightは、AWSが提供するBIツールであり、クラウド上に保存されたデータをもとにダッシュボードやレポートを簡単に作成できるサービスです。

ご注意ください QuickSightは従量課金ではなく月ごとの請求となります。作成しただけで最低でも24 USDが請求されます。
詳細はQuickSightの料金ページをご確認ください。料金が気になる場合は、このセクションはスキップしても問題ありません。

6.1 QuickSightの登録

サインアップ

QuickSightのダッシュボードを開き、「QUICKSIGHTにサインアップ」をクリックします。

下記の内容を設定してください。

設定項目 設定の基準
メールアドレス 任意のメールアドレス QuickSightの通知先として使用するため
認証方法 デフォルト 標準の認証方式を使用するため
QuickSightリージョン Asia Pacific (Tokyo) 東京リージョンで利用するため
QuickSightアカウント名 test-quicksight アカウントを識別するため

続いて、アクセス権限の設定を行います。IAMロールについてはデフォルトを選択し、自動検出を許可するサービスとしてIAMAmazon S3Amazon Athenaを有効にします。

QuickSightがアクセスするS3バケットを選択します。先ほどGlueからのデータを保存したglue-data-(識別子)を指定します。

注意: 「ピクセルパーフェクトレポートを追加」は必ずチェックを外してください。チェックを外さないと500ドル程度の料金が発生するため、注意が必要です。

すべての入力が完了したら「完了」をクリックします。

しばらく経過するとアカウントが作成されるので、「QUICKSIGHTに移動」をクリックします。

新しい分析を作成

QuickSightではAthenaなどのデータソースを利用できます。ここではAthenaをデータソースとして設定し、データの可視化を行えるようにします。

左側のメニューから「分析」をクリックします。

「新しいデータセット」をクリックします。

データソースとして「Athena」を選択します。

データソース名にquicksight-testと入力し、「データソースを作成」をクリックします。

下記の内容を設定してください。

設定項目 設定の基準
カタログ AwsDataCatalog Glueデータカタログを参照するため
データベース default テーブルを登録したデータベースを指定するため
テーブル users 分析対象のテーブルを指定するため

選択が完了したら「選択」をクリックします。

「データクエリを直接実行」を選択し、「Visualize」をクリックします。

💡 ポイント
「データクエリを直接実行」は、可視化するたびにAthenaへクエリを投げて結果を取得する方式です。「迅速な分析のためにSPICEインポート」を選ぶと、データがQuickSight内の高速ストレージに取り込まれ、パフォーマンスが向上します。

6.2 データの分析

分析一覧の中にusers analysisという分析が表示されます。こちらをクリックしてください。

データセットからフィールドを選択することで、円グラフや棒グラフ、テーブルなど任意の可視化を作成できます。以下の画像はgender項目を使って円グラフを作成した例です。

ぜひ、いろいろな分析を試してみましょう。

7. 不要リソースの削除

ハンズオンが完了したら、不要なコストが発生しないよう、作成したリソースを削除してください。CloudFormationで作成したリソース以外を先に削除し、最後にCloudFormationスタックを削除します。

  1. QuickSight — アカウントの削除(QuickSightの管理画面から「アカウントの削除」を実行)
  2. Glueジョブ — 作成したETLジョブ(test-job)を削除
  3. Glueクローラ — test-crawlerを削除
  4. Glue Connection — glue-connectionを削除
  5. Glueデータカタログ — usersテーブルを削除
  6. S3バケット — glue-data-(識別子)バケットを空にしてから削除
  7. VPCエンドポイント — glue-endpointを削除
  8. Glue用リソース — glue-subnetとglue-sgを削除
  9. IAMロール・ポリシー — glue-test-roleとglue-test-policyを削除
  10. CloudFormationスタック — data-analysis-handsonスタックを削除(VPC、RDS、EC2などが一括で削除されます)

注意: 特にQuickSightとRDSは月額料金が発生するため、忘れずに削除してください。CloudFormationスタックの削除前に、スタック外で作成したリソース(手順1〜9)を必ず先に削除してください。

8. まとめ

このハンズオンでは、AWS Glue・Athena・QuickSightを使ったデータ分析基盤の構築を体験しました。

  • CloudFormationでVPC・RDS・EC2の環境をテンプレートから一括構築した
  • 踏み台サーバからRDSに接続し、SQLでテストデータを登録した
  • IAMロール・セキュリティグループでGlueに必要な権限とネットワーク設定を行った
  • AWS GlueのVisual ETLでRDSからデータを抽出し、Parquet形式でS3に格納した
  • GlueクローラでS3上のデータを自動スキャンし、データカタログにテーブル定義を自動作成した
  • Amazon AthenaでS3上のParquetデータにSQLクエリを実行した
  • Amazon QuickSightでAthenaのデータを円グラフや表で可視化した

9. (参考)Terraformで構築する場合

このハンズオンで構築した環境は、Terraformを使ってコードで管理することもできます。以下に、VPC・RDS・EC2を含むすべてのリソースをTerraformで構築するサンプルコードを示します。

注意: QuickSightのセットアップとテストデータの登録(MySQLクライアントのインストール・SQLによるデータ投入)は、Terraformの範囲外のため手動で行う必要があります。

9.1 Terraformコード

以下のコードをmain.tfとして保存してください。

# ==============================================================
# Provider(プロバイダ設定)
# Terraformが使用するAWSプロバイダのバージョンとリージョンを指定
# ==============================================================
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 6.0"
    }
  }
}

provider "aws" {
  region = "ap-northeast-1" # 東京リージョン
}

# ==============================================================
# Variables(入力変数)
# terraform.tfvars または実行時に値を指定する
# ==============================================================

# RDSのマスターパスワード(sensitive = true でログに表示されない)
variable "db_master_password" {
  type        = string
  sensitive   = true
  description = "RDS master user password"
}

# S3バケット名の末尾に付けるAWSアカウントID(バケット名はグローバルで一意にする必要があるため)
variable "s3_bucket_suffix" {
  type        = string
  description = "AWS account ID for S3 bucket suffix (e.g. 123456789012)"
}

# ==============================================================
# VPC(仮想ネットワーク)
# すべてのリソースを配置するネットワーク基盤
# ==============================================================
resource "aws_vpc" "main" {
  cidr_block           = "10.0.0.0/21"
  enable_dns_support   = true  # VPC内でDNS解決を有効化
  enable_dns_hostnames = true  # DNSホスト名の自動割り当てを有効化
  tags                 = { Name = "my-vpc" }
}

# ==============================================================
# Subnets(サブネット)
# 用途ごとにネットワークを分割する
# ==============================================================

# RDS用サブネット1(AZ-a)- RDSのマルチAZ対応のため2つ作成
resource "aws_subnet" "db_01" {
  vpc_id            = aws_vpc.main.id
  cidr_block        = "10.0.0.0/24"
  availability_zone = "ap-northeast-1a"
  tags              = { Name = "db-subnet-01" }
}

# RDS用サブネット2(AZ-c)- DBサブネットグループには2つ以上のAZが必要
resource "aws_subnet" "db_02" {
  vpc_id            = aws_vpc.main.id
  cidr_block        = "10.0.1.0/24"
  availability_zone = "ap-northeast-1c"
  tags              = { Name = "db-subnet-02" }
}

# 踏み台サーバ(EC2)用のパブリックサブネット
resource "aws_subnet" "api" {
  vpc_id                  = aws_vpc.main.id
  cidr_block              = "10.0.2.0/24"
  availability_zone       = "ap-northeast-1a"
  map_public_ip_on_launch = true # パブリックIPを自動付与(EC2 Instance Connect用)
  tags                    = { Name = "api-subnet" }
}

# Glue用のプライベートサブネット(RDSと同じAZに配置)
resource "aws_subnet" "glue" {
  vpc_id            = aws_vpc.main.id
  cidr_block        = "10.0.3.0/24"
  availability_zone = "ap-northeast-1a"
  tags              = { Name = "glue-subnet" }
}

# ==============================================================
# Internet Gateway / Route Table(インターネット接続設定)
# 踏み台サーバからインターネットへの通信経路を確保する
# ==============================================================

# インターネットゲートウェイ(VPCとインターネットの接続口)
resource "aws_internet_gateway" "main" {
  vpc_id = aws_vpc.main.id
  tags   = { Name = "my-internet-gateway" }
}

# パブリックルートテーブル(0.0.0.0/0 → IGWへルーティング)
resource "aws_route_table" "public" {
  vpc_id = aws_vpc.main.id
  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.main.id
  }
  tags = { Name = "api-route-table" }
}

# api-subnetにパブリックルートテーブルを関連付け
resource "aws_route_table_association" "api" {
  subnet_id      = aws_subnet.api.id
  route_table_id = aws_route_table.public.id
}

# ==============================================================
# Security Groups(セキュリティグループ)
# リソースごとに通信を制御するファイアウォール設定
# ==============================================================

# --- RDS用セキュリティグループ ---
# インバウンドルールは別途 aws_security_group_rule で追加
resource "aws_security_group" "db" {
  name        = "db-sg"
  description = "Security group for RDS"
  vpc_id      = aws_vpc.main.id
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
  tags = { Name = "db-sg" }
}

# --- 踏み台サーバ用セキュリティグループ ---
# SSH(ポート22)を許可し、EC2 Instance Connectでのログインを可能にする
resource "aws_security_group" "jump" {
  name        = "jump-sg"
  description = "Security group for Jump Server"
  vpc_id      = aws_vpc.main.id
  ingress {
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
  tags = { Name = "jump-sg" }
}

# --- Glue用セキュリティグループ ---
resource "aws_security_group" "glue" {
  name        = "glue-sg"
  description = "Security group for Glue"
  vpc_id      = aws_vpc.main.id
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
  tags = { Name = "glue-sg" }
}

# Glueの自己参照ルール: ワーカーノード間の通信を許可
# Glue(Spark)は複数ワーカーが並列動作するため、同一SG内の全通信を許可する
resource "aws_security_group_rule" "glue_self" {
  type                     = "ingress"
  from_port                = 0
  to_port                  = 0
  protocol                 = "-1"
  source_security_group_id = aws_security_group.glue.id
  security_group_id        = aws_security_group.glue.id
}

# RDSへの接続許可: 踏み台サーバ → RDS(MySQL 3306番ポート)
resource "aws_security_group_rule" "db_from_jump" {
  type                     = "ingress"
  from_port                = 3306
  to_port                  = 3306
  protocol                 = "tcp"
  source_security_group_id = aws_security_group.jump.id
  security_group_id        = aws_security_group.db.id
}

# RDSへの接続許可: Glue → RDS(MySQL 3306番ポート)
resource "aws_security_group_rule" "db_from_glue" {
  type                     = "ingress"
  from_port                = 3306
  to_port                  = 3306
  protocol                 = "tcp"
  source_security_group_id = aws_security_group.glue.id
  security_group_id        = aws_security_group.db.id
}

# ==============================================================
# RDS(データベース)
# 分析対象のテストデータを格納するMySQLインスタンス
# ==============================================================

# DBサブネットグループ(RDSの配置先サブネットを指定)
resource "aws_db_subnet_group" "main" {
  name       = "my-subnet-group"
  subnet_ids = [aws_subnet.db_01.id, aws_subnet.db_02.id]
  tags       = { Name = "my-subnet-group" }
}

# RDSインスタンス本体
resource "aws_db_instance" "main" {
  identifier             = "testdb"
  engine                 = "mysql"
  instance_class         = "db.t3.micro"
  allocated_storage      = 20               # ストレージ容量(GB)
  username               = "admin"
  password               = var.db_master_password
  db_subnet_group_name   = aws_db_subnet_group.main.name
  vpc_security_group_ids = [aws_security_group.db.id]
  publicly_accessible    = false             # プライベートサブネットに配置
  availability_zone      = "ap-northeast-1a"
  skip_final_snapshot    = true              # 削除時にスナップショットを作成しない(ハンズオン用)
}

# ==============================================================
# EC2(踏み台サーバ)
# RDSにログインしてテストデータを登録するために使用
# ==============================================================

# SSM Parameter Storeから最新のAmazon Linux 2023のAMI IDを取得
data "aws_ssm_parameter" "ami" {
  name = "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-6.1-x86_64"
}

resource "aws_instance" "jump" {
  ami                    = data.aws_ssm_parameter.ami.value
  instance_type          = "t3.micro"
  subnet_id              = aws_subnet.api.id
  vpc_security_group_ids = [aws_security_group.jump.id]
  tags                   = { Name = "JumpServer" }
}

# ==============================================================
# S3 Bucket(データレイク用ストレージ)
# GlueのETL処理で抽出・変換したデータの保存先
# ==============================================================

# Glueの出力先バケット
resource "aws_s3_bucket" "glue_data" {
  bucket = "glue-data-${var.s3_bucket_suffix}"
}

# rawdata/users/ フォルダ(プレフィックス)を作成
# ETL処理の出力先として使用する
resource "aws_s3_object" "rawdata_folder" {
  bucket = aws_s3_bucket.glue_data.id
  key    = "rawdata/users/"
}

# ==============================================================
# IAM(Glue用の権限設定)
# GlueがRDS・S3・CloudWatch Logsなどにアクセスするための権限
# ==============================================================

# Glue用のIAMポリシー
resource "aws_iam_policy" "glue" {
  name = "glue-test-policy"
  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Action = [
          "glue:*",                    # Glueの全操作(ジョブ実行、データカタログ操作)
          "rds:DescribeDBInstances",   # RDSの接続情報を参照
          "ec2:*",                     # VPC内でENI(ネットワークインターフェース)を管理
          "s3:PutObject",              # S3にデータを書き込み
          "s3:PutObjectAcl",           # S3オブジェクトのACLを設定
          "s3:GetObject",              # S3からデータを読み取り
          "s3:ListBucket",             # S3バケットの一覧取得
          "logs:CreateLogGroup",       # CloudWatch Logsにロググループを作成
          "logs:CreateLogStream",      # CloudWatch Logsにログストリームを作成
          "logs:PutLogEvents"          # CloudWatch Logsにログを出力
        ]
        Resource = "*"
      }
    ]
  })
}

# Glue用のIAMロール(信頼ポリシーでGlueサービスからの引き受けを許可)
resource "aws_iam_role" "glue" {
  name = "glue-test-role"
  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect    = "Allow"
        Principal = { Service = "glue.amazonaws.com" }
        Action    = "sts:AssumeRole"
      }
    ]
  })
}

# IAMポリシーをIAMロールにアタッチ
resource "aws_iam_role_policy_attachment" "glue" {
  role       = aws_iam_role.glue.name
  policy_arn = aws_iam_policy.glue.arn
}

# ==============================================================
# VPC Endpoint(S3 Gatewayエンドポイント)
# Glueはプライベートサブネットに配置されるため、
# VPCエンドポイント経由でS3にアクセスする必要がある
# ==============================================================
resource "aws_vpc_endpoint" "s3" {
  vpc_id            = aws_vpc.main.id
  service_name      = "com.amazonaws.ap-northeast-1.s3"
  vpc_endpoint_type = "Gateway"
  # glue-subnetが使用するルートテーブル(VPCのメインルートテーブル)を指定
  route_table_ids   = [aws_vpc.main.main_route_table_id]
  tags              = { Name = "glue-endpoint" }
}

# ==============================================================
# Glue Connection(データベース接続設定)
# GlueからRDSにJDBC接続するための設定
# ==============================================================
resource "aws_glue_connection" "main" {
  name            = "glue-connection"
  connection_type = "JDBC"

  connection_properties = {
    # RDSのエンドポイントはaws_db_instance.main.addressで自動参照
    JDBC_CONNECTION_URL = "jdbc:mysql://${aws_db_instance.main.address}:3306/datalake_test"
    USERNAME            = "admin"
    PASSWORD            = var.db_master_password
  }

  # Glueを配置するネットワーク設定
  physical_connection_requirements {
    availability_zone      = "ap-northeast-1a"
    security_group_id_list = [aws_security_group.glue.id]
    subnet_id              = aws_subnet.glue.id
  }
}

# ==============================================================
# Glue Crawler(クローラ)
# S3上のParquetデータを自動スキャンし、データカタログにテーブル定義を作成
# ==============================================================
resource "aws_glue_crawler" "users" {
  name          = "test-crawler"
  role          = aws_iam_role.glue.arn
  database_name = "default"

  # クローラのスキャン対象(ETLジョブの出力先S3パス)
  s3_target {
    path = "s3://${aws_s3_bucket.glue_data.id}/rawdata/users/"
  }
}

# ==============================================================
# Glue ETL Job(ETLジョブ)
# RDSからデータを抽出し、Parquet形式に変換してS3に格納する
# ※ ハンズオンではVisual ETLで作成するが、Terraformではスクリプトで定義する
# ==============================================================

# ETLスクリプト(PySpark)をS3にアップロード
resource "aws_s3_object" "etl_script" {
  bucket  = aws_s3_bucket.glue_data.id
  key     = "scripts/etl_job.py"
  content = <<-PYTHON
import sys
from awsglue.transforms import *
from awsglue.utils import getResolvedOptions
from pyspark.context import SparkContext
from awsglue.context import GlueContext
from awsglue.job import Job

# ジョブ引数の取得(ジョブ名とS3出力先パス)
args = getResolvedOptions(sys.argv, ['JOB_NAME', 's3_target_path'])

# Spark / Glueコンテキストの初期化
sc = SparkContext()
glueContext = GlueContext(sc)
spark = glueContext.spark_session
job = Job(glueContext)
job.init(args['JOB_NAME'], args)

# RDSからデータを抽出(Glue Connectionを使用してJDBC接続)
datasource = glueContext.create_dynamic_frame.from_options(
    connection_type="mysql",
    connection_options={
        "useConnectionProperties": "true",
        "dbtable": "users",
        "connectionName": "glue-connection"
    }
)

# S3にParquet形式で書き出し
glueContext.write_dynamic_frame.from_options(
    frame=datasource,
    connection_type="s3",
    connection_options={"path": args['s3_target_path']},
    format="parquet"
)

job.commit()
PYTHON
}

# Glue ETLジョブの定義
resource "aws_glue_job" "etl" {
  name     = "test-job"
  role_arn = aws_iam_role.glue.arn

  command {
    name            = "glueetl"
    script_location = "s3://${aws_s3_bucket.glue_data.id}/scripts/etl_job.py"
    python_version  = "3"
  }

  # スクリプトに渡すパラメータ
  default_arguments = {
    "--job-language"   = "python"
    "--s3_target_path" = "s3://${aws_s3_bucket.glue_data.id}/rawdata/users/"
    "--TempDir"        = "s3://${aws_s3_bucket.glue_data.id}/temp/"
  }

  connections       = [aws_glue_connection.main.name]
  glue_version      = "4.0"       # Glue 4.0(Spark 3.3ベース)
  worker_type       = "G.1X"      # ワーカータイプ(G.1X = 4 vCPU, 16 GB メモリ)
  number_of_workers = 2           # ワーカー数(小規模データのため最小構成)
}

# ==============================================================
# Outputs(出力値)
# terraform apply後にコンソールに表示される情報
# ==============================================================
output "rds_endpoint" {
  description = "RDSのエンドポイントアドレス(テストデータ登録時に使用)"
  value       = aws_db_instance.main.address
}

output "jump_server_instance_id" {
  description = "踏み台サーバのインスタンスID(EC2 Instance Connect時に使用)"
  value       = aws_instance.jump.id
}

output "s3_bucket_name" {
  description = "データ格納先のS3バケット名"
  value       = aws_s3_bucket.glue_data.id
}

9.2 Glueリソースの解説

Terraform コードのうち、このハンズオンの主題であるGlue関連のリソースについて解説します。

リソース Terraformリソース名 説明
S3バケット aws_s3_bucket / aws_s3_object Glueが出力するParquetデータの保存先となるS3バケットを作成。aws_s3_objectでバケット内にrawdata/users/フォルダ(プレフィックス)を作成
IAMポリシー aws_iam_policy Glueデータカタログ・RDS・S3・EC2(VPCネットワーキング用)・CloudWatch Logs(実行ログ出力用)へのアクセス権限を定義
IAMロール aws_iam_role / aws_iam_role_policy_attachment Glueジョブおよびクローラの実行に使用するロール。assume_role_policyでGlueサービスからの引き受けを許可し、上記のIAMポリシーをアタッチ
VPCエンドポイント aws_vpc_endpoint GlueはPrivate Subnetに配置されるため、Gateway型のVPCエンドポイントを作成してVPC内部からS3への通信経路を確保。route_table_idsにはGlueサブネットが使用するルートテーブル(VPCのメインルートテーブル)を指定
Glue Connection aws_glue_connection GlueからRDSにJDBC接続するための設定。connection_propertiesにJDBC URL・ユーザ名・パスワードを設定し、physical_connection_requirementsでGlue用のサブネットとセキュリティグループを指定。RDSのエンドポイントはaws_db_instance.main.addressで自動参照されるため、手動でのコピーが不要
Glueクローラ aws_glue_crawler S3上のParquetデータを自動スキャンし、データカタログにテーブル定義(スキーマ)を自動作成する。s3_targetにETLジョブの出力先パス(rawdata/users/)を指定。ハンズオン本編と同じクローラ方式で、手動でのスキーマ定義が不要
ETLスクリプト aws_s3_object(etl_script) PySparkで記述されたETLスクリプトをS3にアップロード。JDBC Connection経由でRDSのusersテーブルからデータを抽出し、S3にParquet形式で保存する。ハンズオンのVisual ETLで自動生成されるスクリプトと同等の処理
Glue ETLジョブ aws_glue_job Glueジョブの定義。スクリプトの場所、IAMロール、Glueバージョン、ワーカータイプ・数などを指定。default_argumentsでスクリプトに渡すパラメータ(出力先S3パスなど)を設定

9.3 実行方法

Terraform実行

terraform.tfvarsを作成し、変数を設定します。

db_master_password = "your-password-here"
s3_bucket_suffix   = "123456789012"  # ご自身のAWSアカウントIDに置き換えてください

まず、Terraformの初期化を行います。プロバイダのダウンロードなどが実行されます。

terraform init

次に、作成されるリソースの実行計画を確認します。どのようなリソースが作成されるかを事前に確認できます。

terraform plan

問題がなければ、実際にリソースを作成します。確認プロンプトが表示されるので、yesと入力してください。

terraform apply

実行が完了すると、RDSのエンドポイントや踏み台サーバのインスタンスIDが出力されます。

以下のコマンドで、RDSのエンドポイントを確認します。この値は次のステップで使用するため、コピーしておいてください。

terraform output rds_endpoint

踏み台サーバにアクセスしてテストデータを登録

踏み台サーバ(JumpServer)にEC2 Instance Connectで接続し、ハンズオン本編と同じ手順でテストデータを登録します。

踏み台サーバに接続したら、MySQLクライアントをインストールします。

sudo dnf install -y mariadb105

RDSに接続します。<RDSのエンドポイント>の部分は、ステップ1で確認したRDSエンドポイントの値に置き換えてください。

mysql -h <RDSのエンドポイント> -u admin -p

パスワードの入力を求められるので、terraform.tfvarsに設定したdb_master_passwordの値を入力します。

接続できたら、データベースを作成します。

CREATE DATABASE datalake_test;

続いて、テーブルを作成します。

CREATE TABLE datalake_test.users (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(255) NOT NULL,
    age INT,
    gender INT
);

最後に、テストデータを投入します。

INSERT INTO datalake_test.users (name, age, gender) VALUES
('Yamada Taro',      28, 1),
('Suzuki Hanako',    25, 2),
('Sato Kenji',       34, 1),
('Tanaka Yuki',      22, 2),
('Kobayashi Shota',  31, 1),
('Watanabe Aiko',    29, 2),
('Ito Naoki',        40, 1),
('Nakamura Mika',    27, 2),
('Kato Kenta',       36, 1),
('Yamamoto Rina',    33, 2);

Glue ETLジョブを実行

Glue ETLジョブはTerraformでリソースを作成しただけでは自動実行されません。AWSマネジメントコンソールから手動で実行します。

AWSマネジメントコンソールで「AWS Glue」を開き、左メニューから「ETL jobs」を選択します。ジョブ一覧からtest-jobを選択し、画面右上の「Run」ボタンをクリックしてジョブを実行します。「Runs」タブでジョブの実行状況を確認でき、ステータスが「Succeeded」になれば完了です。

Glueクローラを実行

ETLジョブが完了したら、S3に出力されたParquetデータのスキーマを自動検出するため、クローラを実行します。

AWSマネジメントコンソールで「AWS Glue」の左メニューから「Crawlers」を選択します。クローラ一覧からtest-crawlerを選択し、「Run」ボタンをクリックして実行します。数分経過するとステータスが「Completed」になり、「Data Catalog」>「Tables」にusersテーブルが自動作成されます。

Athenaでデータを確認

クローラの実行が完了したら、Athenaのダッシュボードを開き、usersテーブルに対してSQLクエリを実行できます。

SELECT * FROM users;
💡 ポイント
QuickSightはTerraformでの管理に対応していないため、可視化を行う場合はハンズオン本編の「データの可視化」セクションの手順に従って手動でセットアップしてください。

リソースの削除

環境を削除する場合は、以下のコマンドを実行します。

terraform destroy
ご注意ください QuickSightを作成した場合は、terraform destroyの前にQuickSightのアカウントを手動で削除してください。QuickSightは月額課金のため、放置すると料金が発生し続けます。