Amazon Athena の テーブル定義のバージョン管理とバックアップ方法


## 背景

Amazon Athena を使うと、S3 上の　CSV ファイル等などの構造化されたデータに対して SQL クエリをかけることができ、便利です。
Athena では、クエリをかける前に、データのテーブル定義（スキーマ）を設定する必要があります。
テーブル定義を設定するためには、マネージメントコンソールなどから手動で設定したり、DDLを実行して設定することもできます。もしくは、Glue クローラを使って自動的に検出することもできます。

一番ありそうなのが、両方使うパタンです。例えばGlue クローラで自動判定したのちに、手動でデータ型などの修正を行う。
もしくは逆に、最初に DDL を実行しておいて、新しく追加されたデータについて Glueクローラでアップデートしていく、など。

そうなっていくと面倒なのが、テーブル定義のバージョン管理やバックアップです。
Amazon Athena 上で見えているテーブル定義を抜き出して、Git / CodeCommit などのツールでバージョン管理をしたいと考えた場合、意外と悩ましかったので、記事にしてみました。


## やりたいこと

Athena 上で見えているテーブル定義を抽出したり、過去のバージョンを書き戻したりする処理を、コマンド一発で実施したい。

## 解決策

Athena は内部的に AWS Glue Data Catalog で管理されているテーブル定義を参照していますので、AWS CLIを使って、AWS Glue Data Catalog のテーブル定義を抽出したり、書き戻したりします。


## Step 0 （準備編）

既存のユーザーIDやテーブル定義がある人は、それを使います。

無い場合、以下のように準備します。

* Athena や Glue へのアクセス権限を持つIAMユーザを作成するか、もしくは既存のユーザに権限をアタッチします。以下の例では `athenaadmin` というユーザを作り、 `AmazonAthenaFullAccess` という既存ポリシーをアタッチしました。
* S3に検索対象のデータのファイルをアップロードします。
* AWS Glue を使用し、公式ドキュメントなどを参考に、クローラを設定してテーブル定義を作成します。
  *  参考: AWS公式ドキュメント "AWS Glue を使用した Amazon S3 内のデータソースへの接続" https://docs.aws.amazon.com/ja_jp/athena/latest/ug/data-sources-glue.html 
* ターミナルなどから AWS CLI にログインし、CLI を使えるようにします。出力形式としては `json` を選択しておきます。


## Step 1 : AWS CLIを使って、データベース一覧を取得する。


AWS CLI で以下のようなコマンドを打ち、データベース一覧を取得します。

### 入力例

```
$ aws glue get-databases
```

### 出力例

例えば `athena-test01-db` と、 `athena_test02` という２つのデータベースが存在している場合、以下のような結果が出力されます。

```
{
    "DatabaseList": [
        {
            "Name": "athena-test01-db",
            "CreateTime": "2022-09-06T12:34:56+09:00",
            "CreateTableDefaultPermissions": [
                {
                    "Principal": {
                        "DataLakePrincipalIdentifier": "IAM_ALLOWED_PRINCIPALS"
                    },
                    "Permissions": [
                        "ALL"
                    ]
                }
            ],
            "CatalogId": "999999999999"
        },
        {
            "Name": "athena_test02",
            "CreateTime": "2022-12-24T00:00:00+09:00",
            "CreateTableDefaultPermissions": [
                {
                    "Principal": {
                        "DataLakePrincipalIdentifier": "IAM_ALLOWED_PRINCIPALS"
                    },
                    "Permissions": [
                        "ALL"
                    ]
                }
            ],
            "CatalogId": "999999999999"
        }
    ]
}
```

## Step 2 : AWS CLIを使って、テーブル一覧を取得する。

### 入力例

```
$ aws glue get-tables --database-name athena_test02
```

### 出力例

例えば、 `test02_input` というテーブルが定義されている場合、上記コマンドを実行すると、以下のようなJSON が出力されます。（実際には、複数のテーブルが定義されているかもしれませんが、長いので省略）

```
{
    "TableList": [
        {
            "Name": "test02_input",
            "DatabaseName": "athena_test02",
            "Owner": "owner",
            "CreateTime": "2022-12-24T00:00:00+09:00",
            "UpdateTime": "2022-12-24T00:00:00+09:00",
            "LastAccessTime": "2022-12-25T00:00:00+09:00",
            "Retention": 0,
            "StorageDescriptor": {
                "Columns": [
                    {
                        "Name": "date",
                        "Type": "string"
                    },
                    {
                        "Name": "productcategories",
                        "Type": "string"
                    },
                    {
                        "Name": "geo",
                        "Type": "string"
                    },
                    {
                        "Name": "revenue",
                        "Type": "double"
                    }
                ],
                "Location": "s3://athena-mylargecsvdata-999999999999/input/",
                "InputFormat": "org.apache.hadoop.mapred.TextInputFormat",
                "OutputFormat": "org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat",
                "Compressed": false,
                "NumberOfBuckets": -1,
                "SerdeInfo": {
                    "SerializationLibrary": "org.apache.hadoop.hive.serde2.lazy.LazySimpleSerDe",
                    "Parameters": {
                        "field.delim": ","
                    }
                },
                "BucketColumns": [],
                "SortColumns": [],
                "Parameters": {
                    "skip.header.line.count": "1",
                    "sizeKey": "7263728",
                    "objectCount": "2",
                    "UPDATED_BY_CRAWLER": "athena_test02_crawler",
                    "CrawlerSchemaSerializerVersion": "1.0",
                    "recordCount": "226991",
                    "averageRecordSize": "32",
                    "CrawlerSchemaDeserializerVersion": "1.0",
                    "compressionType": "none",
                    "classification": "csv",
                    "columnsOrdered": "true",
                    "areColumnsQuoted": "false",
                    "delimiter": ",",
                    "typeOfData": "file"
                },
                "StoredAsSubDirectories": false
            },
            "PartitionKeys": [],
            "TableType": "EXTERNAL_TABLE",
            "Parameters": {
                "skip.header.line.count": "1",
                "sizeKey": "7263728",
                "objectCount": "2",
                "UPDATED_BY_CRAWLER": "athena_test02_crawler",
                "CrawlerSchemaSerializerVersion": "1.0",
                "recordCount": "226991",
                "averageRecordSize": "32",
                "CrawlerSchemaDeserializerVersion": "1.0",
                "compressionType": "none",
                "classification": "csv",
                "columnsOrdered": "true",
                "areColumnsQuoted": "false",
                "delimiter": ",",
                "typeOfData": "file"
            },
            "CreatedBy": "arn:aws:sts::999999999999:assumed-role/AWSGlueServiceRole-athena-test02-role/AWS-Crawler",
            "IsRegisteredWithLakeFormation": false,
            "CatalogId": "999999999999",
            "VersionId": "0"
        }
    ]
}

```

## Step 3 : ファイルのバックアップを行う

Step 2 で出力された　JSON をバックアップしておけば、後ほど加工してテーブルを復元することができます。

AWS Glue 自体でもテーブル定義のバージョン管理を行なっていますが、必要であれば Git / CodeCommit など他のツールでバージョン管理します。


## Step 4: ファイル復元を行う

まず、テキストエディタを開き、ファイル復元のためのシェルスクリプトを編集します。

```
#!/bin/bash

aws glue create-table --database-name athena_test02 --table-input 'ここにJSONを貼り付ける'
```

そこに、 Step 2 の JSON から、テーブル定義１つ分の要素を取り出し、編集します。

* 不要な属性を削除します。具体的には、 `DatabaseName` 、 `CreateTime` 、`UpdateTime` 、`CreatedBy`、 `IsRegisteredWithLakeFormation`、 `CatalogId`、 `VersionId` の７個の属性を削除します。（削除しないと、CLI実行時にエラーになります）
* `Name` 属性の値を、任意のテーブル名に変更します。（以下の例では `test02_input-dup` としました）
* 上記シェルスクリプトの雛形の　`ここにJSONを貼り付ける` の部分に貼り付けます。

完成形は以下のようになります。 ( 仮にファイル名を `create-table.sh` とします )

```
#!/bin/bash

aws glue create-table --database-name athena_test02 --table-input '{
    "Name": "test02_input-dup",
    "Owner": "owner",
    "LastAccessTime": "2022-12-24T00:00:00+09:00",
    "Retention": 0,
    "StorageDescriptor": {
        "Columns": [
            {
                "Name": "date",
                "Type": "string"
            },
            {
                "Name": "productcategories",
                "Type": "string"
            },
            {
                "Name": "geo",
                "Type": "string"
            },
            {
                "Name": "revenue",
                "Type": "double"
            }
        ],
        "Location": "s3://athena-mylargecsvdata-999999999999/input/",
        "InputFormat": "org.apache.hadoop.mapred.TextInputFormat",
        "OutputFormat": "org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat",
        "Compressed": false,
        "NumberOfBuckets": -1,
        "SerdeInfo": {
            "SerializationLibrary": "org.apache.hadoop.hive.serde2.lazy.LazySimpleSerDe",
            "Parameters": {
                "field.delim": ","
            }
        },
        "BucketColumns": [],
        "SortColumns": [],
        "Parameters": {
            "skip.header.line.count": "1",
            "sizeKey": "7263728",
            "objectCount": "2",
            "UPDATED_BY_CRAWLER": "athena_test02_crawler",
            "CrawlerSchemaSerializerVersion": "1.0",
            "recordCount": "226991",
            "averageRecordSize": "32",
            "CrawlerSchemaDeserializerVersion": "1.0",
            "compressionType": "none",
            "classification": "csv",
            "columnsOrdered": "true",
            "areColumnsQuoted": "false",
            "delimiter": ",",
            "typeOfData": "file"
        },
        "StoredAsSubDirectories": false
    },
    "PartitionKeys": [],
    "TableType": "EXTERNAL_TABLE",
    "Parameters": {
        "skip.header.line.count": "1",
        "sizeKey": "7263728",
        "objectCount": "2",
        "UPDATED_BY_CRAWLER": "athena_test02_crawler",
        "CrawlerSchemaSerializerVersion": "1.0",
        "recordCount": "226991",
        "averageRecordSize": "32",
        "CrawlerSchemaDeserializerVersion": "1.0",
        "compressionType": "none",
        "classification": "csv",
        "columnsOrdered": "true",
        "areColumnsQuoted": "false",
        "delimiter": ",",
        "typeOfData": "file"
    }
}'
```

このシェルスクリプトを実行します。

```
$ chmod +x create-table.sh
$ ./create-table.sh
```

特に何もメッセージが出力されなければ、成功です。

マネージメントコンソールから AWS Glue の画面を開き、 テーブル一覧に、今作成したテーブルが表示されていることを確認します。

次に、マネージメントコンソールから Athena の画面を開き、 `test02_input-dup` を使ってクエリが発行できることを確認します。


## 終わりに

ここまで書いてみて、「全然コマンド一発じゃ無いじゃん！」ということに気づいきました。ごめんなさい。

まあ、頑張れば上記のステップを自動的に行うようなコマンドも書けそう、ということでご容赦ください。