Amazon Aurora PostgreSQLゼロETL統合でAmazon Redshiftへシームレスにデータ連携

## はじめに
以下のAWS Blog記事にある通り、2024年10月18日にAmazon Aurora PostgreSQLおよびAmazon DynamoDBのAmazon RedshiftとのゼロETL統合がGAされました。

https://aws.amazon.com/jp/blogs/news/amazon-aurora-postgresql-and-amazon-dynamodb-zero-etl-integrations-with-amazon-redshift-now-generally-available/

本記事ではAmazon Aurora PostgreSQL(以下、Aurora PostgreSQL)とAmazon Redshift(以下、Redshift)とのゼロETL統合について、ドキュメントを参照しながら実装してみます。


## ゼロETL統合とは
複雑なETLパイプラインを構築せずにデータをシームレスにRedshiftへ統合できます。これにより、ETLパイプラインの構築や運用のために割いていた貴重な時間を、Redshiftを使用したデータ分析や機械学習といった本来やりたいことへシフトすることができます。

(参考)ゼロETL統合のアーキテクチャ
![Aurora PostgreSQL zero-ETL integration](https://d2908q01vomqb2.cloudfront.net/887309d048beef83ad3eabf2a79a64a389ab1c9f/2024/10/09/DBBLOG-4409-img1.png)

https://aws.amazon.com/jp/blogs/database/amazon-aurora-postgresql-zero-etl-integration-with-amazon-redshift-is-generally-available/

https://docs.aws.amazon.com/redshift/latest/mgmt/zero-etl-using.html

ゼロETL統合を開始するために、いくつかの前提条件があります。
- Redshiftの前提条件
  - Amazon Redshift ServerlessまたはRA3ノードタイプ
  - 暗号化されていること。 (プロビジョニングされたクラスターを使用している場合)
  - 大文字と小文字の区別が有効になっていること 
- 統合元を削除してもレプリケートされたデータはRedshiftに残る
- 宛先データベースは読み取り専用
- 統合元のテーブルには主キーが必要
- RedshiftはUTF-8文字のみを受け入れるため、ソースで定義された照合順序が尊重されない可能性あり
- ゼロETL統合はRedshiftデータウェアハウスターゲットごとに50まで
- Aurora PostgreSQLから最大100個のデータベースをサポート
- PostgreSQL 16.4以降のバージョンをサポート

他にも考慮事項がありますので使用される際は以下サイトをご参照ください。

https://docs.aws.amazon.com/redshift/latest/mgmt/zero-etl.reqs-lims.html

データ型の違いにも注意しましょう。

https://docs.aws.amazon.com/ja_jp/AmazonRDS/latest/AuroraUserGuide/zero-etl.querying.html#zero-etl.data-type-mapping-postgres

## やってみた
### 事前準備
本記事では統合元にシングルインスタンスのAurora PostgreSQL DBクラスターを、統合先にAmazon Redshift Serverlessを使用しますが、それぞれ統合前のクラスター構築の詳細については説明しません。

#### Aurora PostgreSQL DBクラスターの構築
以下のCloudFormationテンプレートを参考に構築します。
手動構築したものをIaC Generatorで出力して動作するように微修正しています。
エンジンバージョンは「16.4」を使用します。
<details><summary>CloudFormationサンプルコード</summary>

```yaml
AWSTemplateFormatVersion: 2010-09-09
Description: CloudFormation Template for Aurora PostgreSQL creadted by IaC Generator.
Resources:

  VPC01:
    UpdateReplacePolicy: "Delete"
    Type: "AWS::EC2::VPC"
    DeletionPolicy: "Delete"
    Properties:
      CidrBlock: "10.0.5.0/24"
      EnableDnsSupport: true
      InstanceTenancy: "default"
      EnableDnsHostnames: true
      Tags:
      - Value: "aurora-vpc-private"
        Key: "Name"

  Subnet01:
    UpdateReplacePolicy: "Delete"
    Type: "AWS::EC2::Subnet"
    DeletionPolicy: "Delete"
    Properties:
      VpcId:
        Ref: "VPC01"
      MapPublicIpOnLaunch: false
      EnableDns64: false
      AvailabilityZoneId: "apne1-az4"
      PrivateDnsNameOptionsOnLaunch:
        EnableResourceNameDnsARecord: false
        HostnameType: "ip-name"
        EnableResourceNameDnsAAAARecord: false
      CidrBlock: "10.0.5.0/28"
      Ipv6Native: false
      Tags:
      - Value: "aurora-subnet-private-1a"
        Key: "Name"

  Subnet02:
    UpdateReplacePolicy: "Delete"
    Type: "AWS::EC2::Subnet"
    DeletionPolicy: "Delete"
    Properties:
      VpcId:
        Ref: "VPC01"
      MapPublicIpOnLaunch: false
      EnableDns64: false
      AvailabilityZoneId: "apne1-az1"
      PrivateDnsNameOptionsOnLaunch:
        EnableResourceNameDnsARecord: false
        HostnameType: "ip-name"
        EnableResourceNameDnsAAAARecord: false
      CidrBlock: "10.0.5.16/28"
      Ipv6Native: false
      Tags:
      - Value: "aurora-subnet-private-1c"
        Key: "Name"

  Subnet03:
    UpdateReplacePolicy: "Delete"
    Type: "AWS::EC2::Subnet"
    DeletionPolicy: "Delete"
    Properties:
      VpcId:
        Ref: "VPC01"
      MapPublicIpOnLaunch: false
      EnableDns64: false
      AvailabilityZoneId: "apne1-az2"
      PrivateDnsNameOptionsOnLaunch:
        EnableResourceNameDnsARecord: false
        HostnameType: "ip-name"
        EnableResourceNameDnsAAAARecord: false
      CidrBlock: "10.0.5.32/28"
      Ipv6Native: false
      Tags:
      - Value: "aurora-subnet-private-1d"
        Key: "Name"

  SecurityGroup01:
    UpdateReplacePolicy: "Delete"
    Type: "AWS::EC2::SecurityGroup"
    DeletionPolicy: "Delete"
    Properties:
      GroupDescription: "SG for Aurora PostgreSQL"
      GroupName: "aurora-sg"
      VpcId:
        Ref: "VPC01"
      SecurityGroupIngress:
      - IpProtocol: "tcp"
        CidrIp: "10.0.5.0/24"
        FromPort: 5432
        ToPort: 5432
      SecurityGroupEgress:
      - CidrIp: "0.0.0.0/0"
        IpProtocol: "-1"
        FromPort: -1
        ToPort: -1

  RDSDBSubnetGroup01:
    UpdateReplacePolicy: "Delete"
    Type: "AWS::RDS::DBSubnetGroup"
    DeletionPolicy: "Delete"
    Properties:
      DBSubnetGroupDescription: "DB Subnet Group for Aurora PostgreSQL"
      SubnetIds:
      - Ref: "Subnet01"
      - Ref: "Subnet02"
      - Ref: "Subnet03"
      DBSubnetGroupName: "aurora-subnet-group-01"

  RDSMonitoringRole:
    UpdateReplacePolicy: "Delete"
    Type: "AWS::IAM::Role"
    DeletionPolicy: "Delete"
    Properties:
      Path: "/"
      ManagedPolicyArns:
      - "arn:aws:iam::aws:policy/service-role/AmazonRDSEnhancedMonitoringRole"
      MaxSessionDuration: 3600
      RoleName: "rds-monitoring-role-01"
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
        - Action: "sts:AssumeRole"
          Effect: "Allow"
          Principal:
            Service: "monitoring.rds.amazonaws.com"
          Sid: ""

  RDSDBClusterParameterGroup01:
    UpdateReplacePolicy: "Delete"
    Type: 'AWS::RDS::DBClusterParameterGroup'
    DeletionPolicy: "Delete"
    Properties:
      Description: Aurora PostgreSQL Cluster Parameter Group
      Family: aurora-postgresql16
      Parameters:
        rds.logical_replication: 1
        aurora.logical_replication_backup: 0
        aurora.logical_replication_globaldb: 0
        aurora.enhanced_logical_replication: 1

  RDSDBParameterGroup01:
    UpdateReplacePolicy: "Delete"
    Type: 'AWS::RDS::DBParameterGroup'
    DeletionPolicy: "Delete"
    Properties:
      Description: Aurora PostgreSQL Parameter Group
      Family: aurora-postgresql16

  RDSOptionGroup01:
    UpdateReplacePolicy: "Delete"
    Type: "AWS::RDS::OptionGroup"
    DeletionPolicy: "Delete"
    Properties:
      OptionGroupDescription: "Default option group for aurora-postgresql 16"
      OptionConfigurations: []
      MajorEngineVersion: "16"
      EngineName: "aurora-postgresql"
      Tags: []

  RDSDBCluster01:
    UpdateReplacePolicy: "Delete"
    Type: "AWS::RDS::DBCluster"
    DeletionPolicy: "Delete"
    Properties:
      StorageEncrypted: true
      MonitoringRoleArn:
        Fn::GetAtt:
        - "RDSMonitoringRole"
        - "Arn"
      AssociatedRoles: []
      EnableHttpEndpoint: false
      EngineMode: "provisioned"
      Port: 5432
      DBClusterIdentifier: "database01"
      PreferredBackupWindow: "13:10-13:40"
      MonitoringInterval: 60
      NetworkType: "IPV4"
      VpcSecurityGroupIds:
        - Fn::GetAtt:
          - "SecurityGroup01"
          - "GroupId"
      CopyTagsToSnapshot: true
      Engine: "aurora-postgresql"
      Tags: []
      EngineLifecycleSupport: "open-source-rds-extended-support-disabled"
      EngineVersion: "16.4"
      StorageType: "aurora"
      AvailabilityZones:
      - "ap-northeast-1a"
      - "ap-northeast-1c"
      - "ap-northeast-1d"
      PerformanceInsightsRetentionPeriod: 7
      EnableLocalWriteForwarding: false
      PreferredMaintenanceWindow: "sat:14:34-sat:15:04"
      AutoMinorVersionUpgrade: true
      DBSubnetGroupName:
        Ref: "RDSDBSubnetGroup01"
      DeletionProtection: false
      ManageMasterUserPassword: true
      MasterUsername: "postgres"
      ReadEndpoint: {}
      EnableIAMDatabaseAuthentication: false
      DBClusterParameterGroupName:
        Ref: "RDSDBClusterParameterGroup01"
      PerformanceInsightsEnabled: true
      BackupRetentionPeriod: 7
      EnableCloudwatchLogsExports: []

  RDSDBInstance01:
    UpdateReplacePolicy: "Delete"
    Type: "AWS::RDS::DBInstance"
    DeletionPolicy: "Delete"
    Properties:
      DBClusterIdentifier:
        Ref: "RDSDBCluster01"
      DBParameterGroupName:
        Ref: "RDSDBParameterGroup01"
      MultiAZ: false
      Engine: "aurora-postgresql"
      LicenseModel: "postgresql-license"
      EngineVersion: "16.4"
      DBInstanceClass: "db.t4g.large"
      AvailabilityZone: "ap-northeast-1a"
      AutoMinorVersionUpgrade: true
      DBSubnetGroupName:
        Ref: "RDSDBSubnetGroup01"
      DBInstanceIdentifier: "database01-instance01"

Outputs:
  AuroraClusterArn:
    Description: "Aurora DB Cluster Arn"
    Value: !GetAtt RDSDBCluster01.DBClusterArn
    Export:
      Name: "AuroraClusterArn"
```
</details>


#### Auroraクラスターパラメータグループの変更

Auroraクラスターパラメータグループについてカスタムパラメータグループを作成し、レプリケーションに関連する以下のパラメータを設定し、Auroraクラスターへ反映します。

- `rds.logical_replication=1`
- `aurora.enhanced_logical_replication=1`
- `aurora.logical_replication_backup=0`
- `aurora.logical_replication_globaldb=0`

上のCloudFormationテンプレートでは以下の通り設定済みです。
```yaml
  RDSDBClusterParameterGroup01:
    UpdateReplacePolicy: "Delete"
    Type: 'AWS::RDS::DBClusterParameterGroup'
    DeletionPolicy: "Delete"
    Properties:
      Description: Aurora PostgreSQL Cluster Parameter Group
      Family: aurora-postgresql16
      Parameters:
        rds.logical_replication: 1
        aurora.logical_replication_backup: 0
        aurora.logical_replication_globaldb: 0
        aurora.enhanced_logical_replication: 1
```

手動でカスタムパラメータグループを作成して付け替える場合、クラスターへ反映されていないとゼロETL統合を作成するときにエラーになってしまうので、パラメータグループを設定したらクラスターを再起動しておきましょう。

#### 統合元データベース、スキーマ、テーブルの作成
Aurora PostgreSQLに統合元となるデータベースおよびスキーマ、テーブルを作成しておきます。これらはゼロETL統合を作成するときにフィルタ条件として指定します。
データベース管理者`postgres`のパスワードはクラスター作成時にSecrets Managerと統合しているため、Secrets Managerのシークレットを参照してください。

```shell
$ psql -h database01.cluster-xxxxxxxxxxxx.ap-northeast-1.rds.amazonaws.com -p 5432 -U postgres;
Password for user postgres: 
psql (15.8, server 16.4)
WARNING: psql major version 15, server major version 16.
         Some psql features might not work.
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, compression: off)
Type "help" for help.

postgres=> 
postgres=> CREATE DATABASE mydb;
CREATE DATABASE
postgres=> 
postgres=> \l
                                                 List of databases
   Name    |  Owner   | Encoding |   Collate   |    Ctype    | ICU Locale | Locale Provider |   Access privileges   
-----------+----------+----------+-------------+-------------+------------+-----------------+-----------------------
 mydb      | postgres | UTF8     | en_US.UTF-8 | en_US.UTF-8 |            | libc            | 
 postgres  | postgres | UTF8     | en_US.UTF-8 | en_US.UTF-8 |            | libc            | 
 rdsadmin  | rdsadmin | UTF8     | en_US.UTF-8 | en_US.UTF-8 |            | libc            | rdsadmin=CTc/rdsadmin
 template0 | rdsadmin | UTF8     | en_US.UTF-8 | en_US.UTF-8 |            | libc            | =c/rdsadmin          +
           |          |          |             |             |            |                 | rdsadmin=CTc/rdsadmin
 template1 | postgres | UTF8     | en_US.UTF-8 | en_US.UTF-8 |            | libc            | =c/postgres          +
           |          |          |             |             |            |                 | postgres=CTc/postgres
(5 rows)

postgres=> 
postgres=> \c mydb
psql (15.8, server 16.4)
WARNING: psql major version 15, server major version 16.
         Some psql features might not work.
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, compression: off)
You are now connected to database "mydb" as user "postgres".
mydb=> 
mydb=> CREATE SCHEMA myschema;
CREATE SCHEMA
mydb=> 
mydb=> \dn+
                                        List of schemas
   Name   |       Owner       |           Access privileges            |      Description       
----------+-------------------+----------------------------------------+------------------------
 myschema | postgres          |                                        | 
 public   | pg_database_owner | pg_database_owner=UC/pg_database_owner+| standard public schema
          |                   | =U/pg_database_owner                   | 
(2 rows)

mydb=> 
mydb=> SET search_path TO myschema,public;
SET
mydb=> 
mydb=> CREATE TABLE myschema.mytable (ID int NOT NULL, Title VARCHAR(50) NOT NULL, Author VARCHAR(50) NOT NULL,
mydb(> Copyright INT NOT NULL, Genre VARCHAR(50) NOT NULL, PRIMARY KEY (ID));
CREATE TABLE
mydb=> 
mydb=> \dt+
                                      List of relations
  Schema  |  Name   | Type  |  Owner   | Persistence | Access method |  Size   | Description 
----------+---------+-------+----------+-------------+---------------+---------+-------------
 myschema | mytable | table | postgres | permanent   | heap          | 0 bytes | 
(1 row)

mydb=> 
mydb=> INSERT INTO myschema.mytable VALUES (1, 'The Shining', 'Stephen King', 1977, 'Supernatural fiction');
INSERT 0 1
mydb=> 
mydb=> SELECT * FROM mytable;
 id |    title    |    author    | copyright |        genre         
----+-------------+--------------+-----------+----------------------
  1 | The Shining | Stephen King |      1977 | Supernatural fiction
(1 row)

mydb=> 
mydb=> \q
$
```

#### Redshift Serverlessの構築
以下のCloudFormationテンプレートを参考に構築します。
Redshift ServerlessのNamespaceとWorkgroupが作成されます。

<details><summary>CloudFormationサンプルコード</summary>

```yaml
AWSTemplateFormatVersion: 2010-09-09
Description: CloudFormation Template for Redshift Serverless.
Resources:

  VPC01:
    UpdateReplacePolicy: "Delete"
    Type: "AWS::EC2::VPC"
    DeletionPolicy: "Delete"
    Properties:
      CidrBlock: "10.0.6.0/24"
      EnableDnsSupport: true
      InstanceTenancy: "default"
      EnableDnsHostnames: true
      Tags:
      - Value: "redshift-vpc-private"
        Key: "Name"

  Subnet01:
    UpdateReplacePolicy: "Delete"
    Type: "AWS::EC2::Subnet"
    DeletionPolicy: "Delete"
    Properties:
      VpcId:
        Ref: "VPC01"
      MapPublicIpOnLaunch: false
      EnableDns64: false
      AvailabilityZoneId: "apne1-az4"
      PrivateDnsNameOptionsOnLaunch:
        EnableResourceNameDnsARecord: false
        HostnameType: "ip-name"
        EnableResourceNameDnsAAAARecord: false
      CidrBlock: "10.0.6.0/26"
      Ipv6Native: false
      Tags:
      - Value: "redshift-subnet-private-1a"
        Key: "Name"

  Subnet02:
    UpdateReplacePolicy: "Delete"
    Type: "AWS::EC2::Subnet"
    DeletionPolicy: "Delete"
    Properties:
      VpcId:
        Ref: "VPC01"
      MapPublicIpOnLaunch: false
      EnableDns64: false
      AvailabilityZoneId: "apne1-az1"
      PrivateDnsNameOptionsOnLaunch:
        EnableResourceNameDnsARecord: false
        HostnameType: "ip-name"
        EnableResourceNameDnsAAAARecord: false
      CidrBlock: "10.0.6.64/26"
      Ipv6Native: false
      Tags:
      - Value: "redshift-subnet-private-1c"
        Key: "Name"

  Subnet03:
    UpdateReplacePolicy: "Delete"
    Type: "AWS::EC2::Subnet"
    DeletionPolicy: "Delete"
    Properties:
      VpcId:
        Ref: "VPC01"
      MapPublicIpOnLaunch: false
      EnableDns64: false
      AvailabilityZoneId: "apne1-az2"
      PrivateDnsNameOptionsOnLaunch:
        EnableResourceNameDnsARecord: false
        HostnameType: "ip-name"
        EnableResourceNameDnsAAAARecord: false
      CidrBlock: "10.0.6.128/26"
      Ipv6Native: false
      Tags:
      - Value: "redshift-subnet-private-1d"
        Key: "Name"

  RedshiftServerlessSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      VpcId:
        Ref: "VPC01"
      GroupDescription: "SG for Redshift"
      Tags:
      - Value: "redshift-sg"
        Key: "Name"

  RedshiftServerlessWorkGroup:
    Type: AWS::RedshiftServerless::Workgroup
    Properties: 
      WorkgroupName: "custom-workgroup-01"
      BaseCapacity: 32
      ConfigParameters: 
        - ParameterKey: "enable_case_sensitive_identifier"
          ParameterValue: "true"
      EnhancedVpcRouting: false
      NamespaceName: !Ref RedshiftServerlessNamespace
      PubliclyAccessible: false
      SecurityGroupIds: 
        - !Ref RedshiftServerlessSecurityGroup
      SubnetIds: 
        - !Ref Subnet01
        - !Ref Subnet02
        - !Ref Subnet03

  RedshiftServerlessNamespace:
    Type: AWS::RedshiftServerless::Namespace
    Properties: 
      NamespaceName: "custom-namespace-01"
      NamespaceResourcePolicy: 
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: redshift.amazonaws.com
            Action: redshift:AuthorizeInboundIntegration
            Condition:
              StringEquals:
                aws:SourceArn: !ImportValue AuroraClusterArn
          - Effect: Allow
            Principal:
              AWS: !Join [":", ["arn", !Ref AWS::Partition, "iam", "", !Ref AWS::AccountId, "root"]]
            Action: redshift:CreateInboundIntegration
      AdminUsername: "admin"
      AdminUserPassword: "PassW0rd!"
      DbName: !Sub "dev-db"

Outputs:
  RedshiftServerlessNamespaceArn:
    Description: "Redshift Serverless Namespace Arn"
    Value: !GetAtt RedshiftServerlessNamespace.Namespace.NamespaceArn
    Export:
      Name: "RedshiftServerlessNamespaceArn"

```
</details>

#### Redshiftデータウェアハウスで大文字・小文字の区別を有効にする
統合を正常に行うために、Redshiftデータウェアハウスで大文字・小文字を区別するパラメータを有効化する必要があります。`enable_case_sensitive_identifier`パラメータをオンにします。

上のCloudFormationテンプレートでは以下の部分で設定しています。
```yaml
  RedshiftServerlessWorkGroup:
    Type: AWS::RedshiftServerless::Workgroup
    Properties: 
      WorkgroupName: "custom-workgroup-01"
      BaseCapacity: 32
      ConfigParameters: 
        - ParameterKey: "enable_case_sensitive_identifier"
          ParameterValue: "true"
```

AWS CLIコマンドで有効化するには以下のコマンドを実行します。
```shell
aws redshift-serverless update-workgroup \
  --workgroup-name custom-workgroup-01 \
  --config-parameters parameterKey=enable_case_sensitive_identifier,parameterValue=true
```

#### Redshiftデータウェアハウスの認証を構成する
Redshift ServerlessのNamespaceのリソースポリシーで「承認されたプリンシパル」にゼロETL統合を作成するためのアクセス権を付与するIAMプリンシパルのARNまたはAWSアカウントのIDを入力します。
「承認された統合ソース」にゼロETL統合のソースクラスターARN(ここではAurora PostgreSQL DBクラスターのARN)を指定します。

いずれも、上のCloudFormationテンプレートの以下の部分に設定済みです。
```yaml
  RedshiftServerlessNamespace:
    Type: AWS::RedshiftServerless::Namespace
    Properties: 
      NamespaceName: "custom-namespace-01"
      NamespaceResourcePolicy: 
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: redshift.amazonaws.com
            Action: redshift:AuthorizeInboundIntegration
            Condition:
              StringEquals:
                aws:SourceArn: !ImportValue AuroraClusterArn
          - Effect: Allow
            Principal:
              AWS: !Join [":", ["arn", !Ref AWS::Partition, "iam", "", !Ref AWS::AccountId, "root"]]
            Action: redshift:CreateInboundIntegration
```

設定後の画面
![RS-Resource-Policy](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144187/0ceb74c9-9454-5514-e7a1-965c45ee9151.png)

### ゼロETL統合を作成する
#### Aurora PostgreSQLからゼロETL統合を作成する
以下のCloudFormationテンプレートを参考に構築します。
SourceArnとTargetArnは上のCloudFormationテンプレートで作成したスタックでエクスポートしたOutputsをインポートしています。

<details><summary>CloudFormationサンプルコード</summary>

```yaml
AWSTemplateFormatVersion: 2010-09-09
Description: CloudFormation Template for Aurora PostgresSQL and Redshift Zero-ETL integration.
Resources:

  Integration:
    Type: 'AWS::RDS::Integration'
    Properties:
      IntegrationName : "zero-etl-01"
      Description: "Zero ETL Integration to Redshift"
      DataFilter: 'include: mydb.myschema.mytable'
      SourceArn: !ImportValue AuroraClusterArn
      TargetArn: !ImportValue RedshiftServerlessNamespaceArn
```
</details>

`DataFilter`プロパティはMaxwellフィルター構文で記載する必要がありますのでご注意ください。(単にDB名やテーブル名を指定してもエラーとなります。)

https://docs.aws.amazon.com/ja_jp/AmazonRDS/latest/AuroraUserGuide/zero-etl.filtering.html

Aurora PostgreSQL DBクラスターおよびRedshift Serverless Namespaceの「ゼロETL統合」タブにステータスがアクティブの統合が作成されます。

Aurora側
![Zero-ETL-Aurora](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144187/96ef24ad-1541-ea70-e217-76535d7d1675.png)

Redshift側
![Zero-ETL-RS](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144187/823970b3-372f-0542-381e-4f86ce0f39ed.png)

#### Redshiftの統合からデータベースを作成する
Redshift側のゼロETL統合を選択すると、以下のように「データベースの作成が必要です」という警告が表示されるので、「統合からデータベースを作成」を押下します。
![Database-Warn](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144187/aa7b43a9-6648-1c8c-7314-c8811418393e.png)

レプリケートされたソースDB名を選択し、Redshift側のデータベース名を入力して「データベースの作成」を押下します。
![Create-RH-DB](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144187/fadd9ba8-0cec-b92e-274d-2c9962e49105.png)

#### データ複製の確認
Redshift Serverless画面の左ペインで「クエリエディタ v2」を選択して開きます。
先ほど作成したデータベースを選択し、クエリエディタにクエリを入力して「Run」を押下すると下画面に結果が表示されます。事前にAurora PostgreSQLへ挿入した行が表示されることを確認できました。

```sql:query
SELECT * FROM myschema.mydb;
```
![Query-Editor-v2](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144187/5a341102-2d45-7edb-47c9-e0b6c254f6b0.png)

### いろいろ試してみる
#### 日本語を挿入
日本語のような2バイト文字があっても問題なく処理されます。

Aurora側
```shell
mydb=> SELECT * FROM myschema.mytable;
 id |    title    |    author    | copyright |        genre         
----+-------------+--------------+-----------+----------------------
  1 | The Shining | Stephen King |      1977 | Supernatural fiction
(1 row)

mydb=> 
mydb=> INSERT INTO myschema.mytable VALUES (2, 'こころ', '夏目漱石', 1914, '長編小説');
INSERT 0 1
mydb=> 
mydb=> SELECT * FROM myschema.mytable;
 id |    title    |    author    | copyright |        genre         
----+-------------+--------------+-----------+----------------------
  1 | The Shining | Stephen King |      1977 | Supernatural fiction
  2 | こころ      | 夏目漱石     |      1914 | 長編小説
(2 rows)

mydb=> 
```

Redshift側
![Japanese_Insert](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144187/846ba5aa-87fb-c0d8-3f9d-cfe69b5ab376.png)

#### 統合元データベースのスキーマ変更
PostgreSQLの論理レプリケーションではDDLの変更はレプリケーションされないのですが、ゼロETL統合では拡張DDLイベントのサポートによりDDLもレプリケーションされます。
※特定位置への列追加など、テーブルが`ResyncRequired`となるようなDDLもあるようなので、変更時はトラブルシューティングのドキュメントをご確認ください。

Aurora側
```shell
mydb=> SELECT * FROM myschema.mytable;
 id |    title    |    author    | copyright |        genre         
----+-------------+--------------+-----------+----------------------
  1 | The Shining | Stephen King |      1977 | Supernatural fiction
  2 | こころ      | 夏目漱石     |      1914 | 長編小説
(2 rows)

mydb=> 
mydb=> ALTER TABLE myschema.mytable ADD COLUMN Country VARCHAR(50) NOT NULL DEFAULT 'N/A';
ALTER TABLE
mydb=> 
mydb=> SELECT * FROM myschema.mytable;
 id |    title    |    author    | copyright |        genre         | country 
----+-------------+--------------+-----------+----------------------+---------
  1 | The Shining | Stephen King |      1977 | Supernatural fiction | N/A
  2 | こころ      | 夏目漱石     |      1914 | 長編小説             | N/A
(2 rows)

mydb=> 
```

Redshift側
![Alter_Tabel](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144187/7389cdf6-f5a1-4202-c34a-c8a8ae3c2b8a.png)

#### 行の削除
行の削除ももちろんできます。

Aurora側
```shell
mydb=> SELECT * FROM myschema.mytable;
 id |    title    |    author    | copyright |        genre         | country 
----+-------------+--------------+-----------+----------------------+---------
  1 | The Shining | Stephen King |      1977 | Supernatural fiction | N/A
  2 | こころ      | 夏目漱石     |      1914 | 長編小説             | N/A
(2 rows)

mydb=> 
mydb=> DELETE FROM myschema.mytable WHERE id = 2;
DELETE 1
mydb=> 
mydb=> SELECT * FROM myschema.mytable;
 id |    title    |    author    | copyright |        genre         | country 
----+-------------+--------------+-----------+----------------------+---------
  1 | The Shining | Stephen King |      1977 | Supernatural fiction | N/A
(1 row)

mydb=> 
```

Redshift側
![Delete_Row](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144187/dad9b695-001b-8879-a990-59d99d1529d5.png)

#### 統合元データベースの停止
ゼロETL統合を有効化した状態で、統合元のAurora PostgreSQL DBクラスターを停止することができます。DBクラスターを起動するとレプリケーションが再開されます。

### ゼロETL統合の監視
Redshiftの以下のシステムビューからゼロETL統合に関する情報を表示できます。
`SVV_INTEGRATION`：統合の設定の詳細
`SYS_INTEGRATION_ACTIVITY`：完了した統合
`SVV_INTEGRATION_TABLE_STATE`：統合の状態
`SYS_INTEGRATION_TABLE_STATE_CHANGE`：統合のテーブル状態変更ログ

統合の状態(`SVV_INTEGRATION_TABLE_STATE`)を表示した画面
![SVV_INTEGRATION_TABLE_STATE](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144187/d84a91fe-0be9-d12c-96d2-9c29a9626f2f.png)

CloudWatchでのメトリクス監視やEventBridge連携もできます。

### トラブルシューティング
ゼロETL統合に関するトラブルシューティングは以下のサイトにまとまっています。

https://docs.aws.amazon.com/ja_jp/AmazonRDS/latest/AuroraUserGuide/zero-etl.troubleshooting.html

#### ゼロETL統合を削除する前にDBインスタンスを削除してしまったとき
私がやってしまった失敗として、ゼロETL統合を削除せずに統合元のDBインスタンスを削除してしまい、DBクラスターを削除しようとすると**ゼロETL統合が残っているため削除できない**といった事象が発生しました。
このような状態になった場合は、AWSコンソールにゼロETL統合を表示できない(DBインスタンスの設定である)ため、AWS CLIでゼロETL統合を削除しましょう。

```shell:ゼロETL統合の表示
aws rds describe-integrations
```
<details><summary>実行結果</summary>

```shell
$ aws rds describe-integrations
{
    "Integrations": [
        {
            "SourceArn": "arn:aws:rds:ap-northeast-1:xxxxxxxxxxxx:cluster:database01",
            "TargetArn": "arn:aws:redshift-serverless:ap-northeast-1:xxxxxxxxxxxx:namespace/yyyyyyyyyyyyyyyyyyyyyyy",
            "IntegrationName": "zero-etl-01",
            "IntegrationArn": "arn:aws:rds:ap-northeast-1:xxxxxxxxxxxx:integration:zzzzzzzz-zzzz-zzzz-zzzz-zzzzzzzzzzzzzzzz",
            "KMSKeyId": "arn:aws:kms:ap-northeast-1:xxxxxxxxxxxx:key/yyyyyyyyyyyyyyyyyyyyyyy",
            "AdditionalEncryptionContext": {},
            "Status": "active",
            "Tags": [
                {
                    "Key": "aws:cloudformation:stack-id",
                    "Value": "arn:aws:cloudformation:ap-northeast-1:xxxxxxxxxxxx:stack/integration-zero-etl-01/721a8490-zzzz-zzzz-zzzz-zzzzzzzzzzzzzz"
                },
                {
                    "Key": "aws:cloudformation:stack-name",
                    "Value": "integration-zero-etl-01"
                },
                {
                    "Key": "aws:cloudformation:logical-id",
                    "Value": "Integration"
                }
            ],
            "CreateTime": "2024-11-27T13:42:46.431000+00:00",
            "DataFilter": "include: mydb.myschema.mytable",
            "Description": "Zero ETL Integration to Redshift"
        }
    ]
}
```
</details>

表示された結果で`IntegrationArn`の中の「integration:」の後ろが`integration-identifier`の値です。

```shell:ゼロETL統合の削除
aws rds delete-integration --integration-identifier xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
```

## Aurora PostgreSQLゼロETL統合の料金
ゼロETL統合自体は無料で利用できます。

>AWS は、ゼロ ETL 統合について追加料金を請求しません。ゼロ ETL 統合の一環として作成された変更データの作成と処理に使用された既存のリソースについての料金はお支払いいただきます。

https://aws.amazon.com/jp/redshift/pricing/

## おわりに
ETLパイプラインを介さずに手軽にRedshiftと統合できるのは嬉しい機能ですね。Aurora MySQLやDynamoDBもゼロETL統合できますので多くのデータをRedshiftに統合して分析基盤の整備に活用しましょう。

最後までご覧いただきありがとうございました。
本記事が皆さまのお役に立てましたら幸いです。