Glueの使い方的な①(GUIでジョブ実行)

# GUIによる操作でGlueジョブを作って実行する
### ジョブの内容

<b>"S3の指定した場所に配置したcsvデータを指定した場所にparquetとして出力する"<b>

### ジョブ名
se2_job0

### クローラー名
se2_in0
se2_out0

# 全体の流れ

* 前準備
* クローラー作成と実行、Athenaで確認
* ジョブの作成と実行、Athenaで確認
* 出来上がったPySparkスクリプト確認

# 前準備

### ジョブで使うIAM role
以下のポリシーを付与した任意の名前のロールを作っておく。今回はtest-glueという名前にした。
・AmazonS3FullAccess
・AWSGlueServiceRole

Glueとの信頼関係も設定しておく

```json:
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "glue.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
```

※権限は必要に応じてより厳しくしてください。今回は検証のため緩めにしてあります。

### 今回使うサンプルログファイル(19件)

```csv:csvlog.csv
deviceid,uuid,appid,country,year,month,day,hour
iphone,11111,001,JP,2017,12,14,12
android,11112,001,FR,2017,12,14,14
iphone,11113,009,FR,2017,12,16,21
iphone,11114,007,AUS,2017,12,17,18
other,11115,005,JP,2017,12,29,15
iphone,11116,001,JP,2017,12,15,11
pc,11118,001,FR,2017,12,01,01
pc,11117,009,FR,2017,12,02,18
iphone,11119,007,AUS,2017,11,21,14
other,11110,005,JP,2017,11,29,15
iphone,11121,001,JP,2017,11,11,12
android,11122,001,FR,2017,11,30,20
iphone,11123,009,FR,2017,11,14,14
iphone,11124,007,AUS,2017,12,17,14
iphone,11125,005,JP,2017,11,29,15
iphone,11126,001,JP,2017,12,19,08
android,11127,001,FR,2017,12,19,14
iphone,11128,009,FR,2017,12,09,04
iphone,11129,007,AUS,2017,11,30,14
```

#### S3に配置

```shell:
$ aws s3 ls s3://test-glue00/se2/in0/
2018-01-02 15:13:27          0 
2018-01-02 15:13:44        691 cvlog.csv
```

### ディレクトリ構成
in0に入力ファイル、out0に出力ファイル

```
$ aws s3 ls s3://test-glue00/se2/
                           PRE in0/
                           PRE out0/
                           PRE script/
                           PRE tmp/
```

# クローラー作成と実行、Athenaで確認

ジョブ作成の前に、こちらもGlueの重要な機能の１つであるData CatalogのCrawlerを作成して動かしていきます。
<b>クローラはデータをスキャン、分類、スキーマ情報を自動認識し、そのメタデータをデータカタログに保存する機能です。これを使って入力となるサンプルデータのスキーマを事前に抽出してデータカタログにメタデータとして保存します。Athena、EMR、RedshiftからもこのGlueのデータカタログを使えます。Hive互換のメタストアです(Hiveメタストアのマネージドサービスと思ってもらえればいいと思います)。<b>

基本的な操作はGUIを使って行えます。

AWSマネージメントコンソールから、Glueをクリックし、画面左側メニューの"Crawlers"をクリックし、"Add crawler"をクリック
![スクリーンショット 0029-12-28 13.24.04.png](https://qiita-image-store.s3.amazonaws.com/0/27932/a12dd385-b815-8f8e-98a9-f70b07e09121.png)

クローラーの名前入力
![スクリーンショット 0029-12-28 11.19.32.png](https://qiita-image-store.s3.amazonaws.com/0/27932/2376b2a2-08a6-e1e4-434f-295aefd10b97.png)

S3にあるソースデータのパス入力(今回はS3に配置してあるデータが対象)
![スクリーンショット 0030-01-02 15.17.11.png](https://qiita-image-store.s3.amazonaws.com/0/27932/805bc53a-54d5-513f-dd6c-7f93efdbabb7.png)

そのまま"Next"
![スクリーンショット 0029-12-28 9.38.41.png](https://qiita-image-store.s3.amazonaws.com/0/27932/a84bdcec-cf97-a495-5a2d-3e8b84e65a85.png)

"Choose an existing IAM role"にチェックを入れ、IAM roleをプルダウンからtest-glueを選択する
![スクリーンショット 0030-01-02 15.17.49.png](https://qiita-image-store.s3.amazonaws.com/0/27932/2a79eb20-9c86-2cb8-fcab-009d96bf03c7.png)

"Run on demand"にチェックを入れ"Next"(今回のクローラーはスケジュールせずに手動実行とする)
![スクリーンショット 0029-12-28 9.39.03.png](https://qiita-image-store.s3.amazonaws.com/0/27932/b480cf63-aa16-5b84-61cd-076740823940.png)

スキーマ情報を保存するDatabaseを選択、既存のものがあればそれでもいいし、なければ下の"Add database"でdatabase作成しそれを選択
Prefixは作成されるテーブル名の先頭に付くもの。見分け分類しやすいものにしておくと良いと思います(現状テーブルにtagとかつけられないので)
![スクリーンショット 0029-12-28 9.39.23.png](https://qiita-image-store.s3.amazonaws.com/0/27932/2ef44b3d-6c94-df78-42ce-41c5117fb1f5.png)

### クローラー実行

対象のクローラーにチェックを入れ、"Run Crawler"をクリック
![スクリーンショット 0030-01-01 17.02.58.png](https://qiita-image-store.s3.amazonaws.com/0/27932/ebfe35df-a6bf-e572-73a5-3fabc3ed876c.png)

### テーブルが出来上がる

"se2_"のPrefixが付いた"se2_in0"のテーブルができています。"in0"は指定した"Include Path"の一番下のディレクトリ名です。指定した"Include Path"(ソースデータがあるS3のパス、画像の"Location"の部分に表示されている)の配下のディレクトリは自動でパーティションとして認識されます。今回は配下にディレクトリはありませんのでパーティションも作成されません。
![スクリーンショット 0030-01-02 15.21.15.png](https://qiita-image-store.s3.amazonaws.com/0/27932/2f6c01ae-1b5f-06b9-ff03-dff73e023fd9.png)

テーブルの内容を確認するとスキーマが自動で作成されています
実データ配置場所のLocationがs3://test-glue00/se2/in0
Table名のNameが、prefixのse2_とInclude Pathで指定したs3://test-glue00/se2/in0の一番下のディレクトリのin0でse2_in0

Schemaを見るとuuidやappidなどがbigintで数値型になってます、文字列型がよければここでも修正できます。
今回は一旦このまま進めます
※本来はClassifierでいい感じにしたほうがいいと思う

![スクリーンショット 0030-02-13 19.30.32.png](https://qiita-image-store.s3.amazonaws.com/0/27932/b397d13c-0aea-0c5c-8673-1f7fd8e0eb57.png)

画面右上の、"View properties"でメタ情報をjsonで見れます。"Compare versions"でテーブルの過去のバージョンを比較して見れます。"Edit schema"でスキーマを更新できます。
~~テーブルはバージョン管理されていて過去のバージョンと差分を比較して見ることはできますが過去のバージョンに戻す機能は現在はありません。~~
【上記誤りでした。訂正します】

![スクリーンショット 0030-01-07 11.01.34.png](https://qiita-image-store.s3.amazonaws.com/0/27932/0dc0a1c6-81b9-d69d-ccfa-205a4c04412e.png)

テーブルはバージョン管理されていて過去のバージョンと差分を比較して見ることはでき、過去のバージョンに戻すこともできます。戻し方は画面右上のバージョンをクリックし戻したいバージョンの番号をクリック、"Edit schema"をクリックし"Save"をクリックすると選択したバージョンに戻った状態となります。

![スクリーンショット 0030-02-13 19.43.15.png](https://qiita-image-store.s3.amazonaws.com/0/27932/fb8a9280-2c16-ee98-f127-34b2959a12c4.png)

【↑誤りを修正したため時間が他の画像と異なってます】

"Veiw properties"をクリックするとテーブルのDDLを確認できます。
今回の入力データの１行目にフィールド名情報が入っていました。１行目をデータとみなさないための設定で"skip.header.line.count:1"が自動で入っていることも確認できます。

![スクリーンショット 0030-02-14 12.30.58.png](https://qiita-image-store.s3.amazonaws.com/0/27932/5b366ed8-7cab-f32e-077e-f64068f87bad.png)

AthenaからもGlueのData Catalog使えます
Athenaから同様のテーブルとスキーマの内容が確認できます。Athenaがスキーマ情報にGlueのデータカタログを使ってることがわかります。画面右上にもGlue Data Catalogへのショートカットもありますね。
![スクリーンショット 0030-01-02 15.27.07.png](https://qiita-image-store.s3.amazonaws.com/0/27932/44ce05ee-f6ac-2034-9f3f-91a5c0011ff9.png)

もちろんクエリ実行もできます。
![スクリーンショット 0030-01-02 15.29.31.png](https://qiita-image-store.s3.amazonaws.com/0/27932/edfc8469-3019-fc4f-85d9-2fe1a8a86e69.png)

# ジョブの作成と実行、Athenaで確認

今回のようなジョブであれば、基本は画面ポチポチするだけです

Glueのメニューから"ETL"の"Job"をクリックし、"Add job"をクリック
![スクリーンショット 0030-01-01 17.36.49.png](https://qiita-image-store.s3.amazonaws.com/0/27932/8e6a84bb-a951-cf4e-ccdb-b7ad3e09148b.png)

"Name"にジョブ名を入れ、"IAM role"はクローラーでも使ったロールを指定、"Temporary directory"は任意の場所で構いません。
![スクリーンショット 0030-01-02 15.31.34.png](https://qiita-image-store.s3.amazonaws.com/0/27932/29f2f413-a218-4423-a4f2-ed0db03c5d24.png)

ソースデータとなるテーブルを選択。(先程作成したテーブルをクリック)
![スクリーンショット 0030-01-02 15.33.35.png](https://qiita-image-store.s3.amazonaws.com/0/27932/9384e474-a97e-2b17-4c8c-ba5a1e31a69a.png)

ターゲットとなるテーブルは作成していないのでここで作ります。
今回は、保存先のData storeは"S3"、出力ファイルフォーマットのFormatは"Parquet"、出力先のパスのTargetPathは"任意の場所"を指定
※圧縮も選べます
![スクリーンショット 0030-01-02 15.34.41.png](https://qiita-image-store.s3.amazonaws.com/0/27932/53e2e21e-e0cc-9d1b-1b3c-2e49a60657b5.png)

ソースデータとターゲットデータのマッピング変換ができます。いらないカラムを出力から除外したり、カラムの順序を変えたり、"Data Type"をstring,long,intなどに変えたり、新しいカラムを追加してそこに出力させる(カラム名を変える時に使えそうです)などができます
![スクリーンショット 0029-12-28 10.08.35.png](https://qiita-image-store.s3.amazonaws.com/0/27932/979c0708-3ddc-d585-66cb-8f832a2e876e.png)

次にサマリがでますので問題なければ"Finish"をクリック

### ジョブの実行

作成したジョブにチェックを入れ、"Action"から"Run job"をクリック
![スクリーンショット 0029-12-28 10.27.38.png](https://qiita-image-store.s3.amazonaws.com/0/27932/376631bb-3d0b-27e7-88af-215ba8115ecf.png)

数分待って以下のように"Run status"が"Succeeded"となれば問題なく完了しています。
※問題があれば、"Error"の箇所にエラーの概要、"Logs"、"Error logs"の箇所の出力がリンクになっていてクリックするとCloudWatch logsに移動します。GlueのログはデフォルトでCloudWatch logsに出力されます
![スクリーンショット 0030-01-02 15.46.47.png](https://qiita-image-store.s3.amazonaws.com/0/27932/b3b1dd90-3c53-c1db-2514-18d8db277dc4.png)

出力したParquetフォーマットのファイルを、ソースデータと同様にクローラーを使ってスキーマを作り、スキーマオンリードでAthenaクエリを実行してみます。クローラー作成手順は前回と同様なので割愛します。
クローラーにより自動作成されたスキーマ
![スクリーンショット 0030-01-02 15.49.41.png](https://qiita-image-store.s3.amazonaws.com/0/27932/560d9b0f-be2d-ca2a-b02e-94d5d395d14e.png)

クエリ結果です。データ量が少なくselectしてるだけなのでアレですが、parquetになったので列単位での集計処理などが高速化されるデータフォーマットに変換ができました。
![スクリーンショット 0030-01-02 15.51.00.png](https://qiita-image-store.s3.amazonaws.com/0/27932/a6488d0f-a354-d14c-744f-bc6ac1757198.png)

<b>"S3の指定した場所に配置したcsvデータを指定した場所にparquetとして出力する"くらいであればGlueはGUIだけでサーバーレスでできます。<b>

# 出来上がったPySparkスクリプト確認

```python:se2_job0.py
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

## @params: [JOB_NAME]
args = getResolvedOptions(sys.argv, ['JOB_NAME'])

sc = SparkContext()
glueContext = GlueContext(sc)
spark = glueContext.spark_session
job = Job(glueContext)
job.init(args['JOB_NAME'], args)
## @type: DataSource
## @args: [database = "se2", table_name = "se2_in0", transformation_ctx = "datasource0"]
## @return: datasource0
## @inputs: []
datasource0 = glueContext.create_dynamic_frame.from_catalog(database = "se2", table_name = "se2_in0", transformation_ctx = "datasource0")
## @type: ApplyMapping
## @args: [mapping = [("deviceid", "string", "deviceid", "string"), ("uuid", "long", "uuid", "long"), ("appid", "long", "appid", "long"), ("country", "string", "country", "string"), ("year", "long", "year", "long"), ("month", "long", "month", "long"), ("day", "long", "day", "long"), ("hour", "long", "hour", "long")], transformation_ctx = "applymapping1"]
## @return: applymapping1
## @inputs: [frame = datasource0]
applymapping1 = ApplyMapping.apply(frame = datasource0, mappings = [("deviceid", "string", "deviceid", "string"), ("uuid", "long", "uuid", "long"), ("appid", "long", "appid", "long"), ("country", "string", "country", "string"), ("year", "long", "year", "long"), ("month", "long", "month", "long"), ("day", "long", "day", "long"), ("hour", "long", "hour", "long")], transformation_ctx = "applymapping1")
## @type: ResolveChoice
## @args: [choice = "make_struct", transformation_ctx = "resolvechoice2"]
## @return: resolvechoice2
## @inputs: [frame = applymapping1]
resolvechoice2 = ResolveChoice.apply(frame = applymapping1, choice = "make_struct", transformation_ctx = "resolvechoice2")
## @type: DropNullFields
## @args: [transformation_ctx = "dropnullfields3"]
## @return: dropnullfields3
## @inputs: [frame = resolvechoice2]
dropnullfields3 = DropNullFields.apply(frame = resolvechoice2, transformation_ctx = "dropnullfields3")
## @type: DataSink
## @args: [connection_type = "s3", connection_options = {"path": "s3://test-glue00/se2/out0"}, format = "parquet", transformation_ctx = "datasink4"]
## @return: datasink4
## @inputs: [frame = dropnullfields3]
datasink4 = glueContext.write_dynamic_frame.from_options(frame = dropnullfields3, connection_type = "s3", connection_options = {"path": "s3://test-glue00/se2/out0"}, format = "parquet", transformation_ctx = "datasink4")
job.commit()
```

### ビルトイン変換のいくつか補足

* ApplyMapping：ソースの列とデータ型をターゲットの列とデータ型にマップします

* ResolveChoice：複数の型の値が含まれている場合の列の処理方法を指定します。列を単一のデータ型にキャストするか、1つ以上の型を破棄するか、またはすべての型を別々の列または構造体に保持するかを選択できます。choiceはspecが空の場合のデフォルトのresolution actionです
    * キャストする型を指定できます（例、cast：int）。曖昧な列(たとえばstringとintがまざってる)をcastで指定した型にする。
    * make_structは構造体を使用してデータを表現することにより、潜在的なあいまいさを解消します。曖昧な列(たとえばstringとintがまざってる)を、DynamicFrameにintとstringの両方を含む構造体の列が生成されます。
    * make_colsはデータを平坦化することによって潜在的なあいまいさを解消します。曖昧な列(たとえばstringとintがまざってる)を、DynamicFrameにcolumnA_intおよびcolumnA_stringという名前の2つの列が生成されます。
    * projectは可能性のあるデータ型の1つにすべてのデータを投影して、潜在的なあいまいさを解決します。曖昧な列(たとえばstringとintがまざってる)を、DynamicFrameに列が生成され、すべてのint値が文字列に変換されます。

* DropNullFields：nullフィールドを削除します

## To Be Continue

よくありそうな変換処理ケースを今後書いていければと思います。

ジョブを作る際に似たようなジョブを作りたい、テスト段階でジョブのパラメータを一部だけ変えたジョブを作りたいことあると思います。現在はジョブのコピーがGUIからはできないのでそのあたりの運用を考慮する場合はCLIの利用がおすすめです。またあとで書きます。

## こちらも是非

似た内容ですがより丁寧にかかれています。安定のクラメソブログ
https://dev.classmethod.jp/cloud/aws/aws-glue-released/
https://dev.classmethod.jp/cloud/aws/aws-glue-tutorial/

Built-In Transforms
https://docs.aws.amazon.com/ja_jp/glue/latest/dg/built-in-transforms.html
https://docs.aws.amazon.com/ja_jp/glue/latest/dg/aws-glue-api-crawler-pyspark-transforms-ApplyMapping.html

Glueの使い方まとめ
https://qiita.com/pioho07/items/32f76a16cbf49f9f712f