S3の署名バージョンの対応状況をAthenaで眺めたい


初AWSです。わけあってAWS S3の署名バージョンの対応をやってました。

(6/18 追記)S3の署名バージョン2の廃止について、時期や内容が変更になりました

以下引用

改訂されたプラン — 2020年6月24日以降に作成された新しいバケットは SigV2 署名付きリクエストはサポートされません。ただし、既存のバケットについて引き続き SigV2 がサポートされますが、我々はお客様が古いリクエスト署名方法から移行するよう働きかけます。

新規のバケットは2020/6以降はSigV2は利用できなくなり、既存のバケットは2020/6以降もSigV2で動く、という感じに読み取れます。
どちらにしてもSDKは今後もアップデートされるので出来るならやってしまったほうが良いでしょう。今回1.x=>3.xにしましたが、名前が変わったり、値の初期化がコンストラクタからプロパティで初期化する形に変わったり、timeout値などのオプションの設定の仕方が変わったAPIがあったり、コールバック引数がちょっと変わるぐらいで、ロジックに大きな影響を与えるものはないはずです。

(追記ここまで)

確認する方法は色々あり、時機も時期ですから、既に有益な情報はたくさんあります。
特にクラスメソッドさんのBlogにはお世話になりました。こんな記事より、まずはそちらをご覧ください。

さて、今回は署名バージョンに新しいもの(SigV4)が使えているかを確認したくて、比較的簡単に出来る方法を探りました。

ちなみに、仕組み上はログが取れないこともあるようなので、100%起きてない事を確認したい場合には使えないと思います。
(そこまで厳密に確認したいならHTTPプロキシを置いて全リクエストをトレースすればいいと思います。)

S3 Bucketのアクセスログ機能を使う

一番(設定が)簡単なのは、もう1個アクセスログ用のS3バケットを作り、操作対象のS3バケットのアクセスログ機能を有効にしてアクセスログ用のS3バケットに流す方法が一番簡単です。画面上からもポチポチするだけで作れて簡単です。

しかし、このチェック作業がしんどい。
1操作1ファイルぐらいの粒度で出るのと、ブラウザ上だとファイルの内容を見て戻ると画面が初期状態に戻るのでサクサク次を見ることができない。

手元で開発して確認ぐらいの用途であればこれでも良さそうですが、システムテストの結果として確認するには辛そうです。
もっと良い方法はないでしょうか。

S3 Bucketのアクセスログ機能 + Athena

AWS AthenaでS3のファイル内容をSQLで分析っぽいことが出来る事を知りました。

S3のアクセスログは出力内容は1行1リクエストというような感じで一貫性があるため、Athenaで読み取れます。

Athenaは1行ずつ取り込むときにJSON,CSVなど方法をいくつか選べて、今回は正規表現を使いました。

以下、Athenaのテーブル定義。s3://BUCKET_NAME/PREFIX/の部分はいい感じに置き換えてください。

CREATE EXTERNAL TABLE IF NOT EXISTS default.accesslog (
  `backet_owner` string,
  `backet_name` string,
  `timestamp` string,
  `remote_ip` string,
  `requester` string,
  `request_id` string,
  `operation` string,
  `key` string,
  `request_uri` string,
  `http_status` int,
  `error_code` string,
  `bytes_sent` int,
  `object_size` int,
  `total_time` int,
  `turnaround_time` int,
  `referrer` string,
  `user_agent` string,
  `version_id` string,
  `unknown1` string,
  `signature_version` string,
  `algorithm` string,
  `unknown2` string,
  `endpoint` string,
  `tls_version` string 
)  ROW FORMAT SERDE 'org.apache.hadoop.hive.serde2.RegexSerDe'
WITH SERDEPROPERTIES (
  'serialization.format' = '1',
  'input.regex' = '(\\S+) (\\S+) \\[(\\S+ \\S+)\\] (\\S+) (\\S+) (\\S+) (\\S+) (\\S+) \"([^\"]+)\" (\\S+) (\\S+) (\\S+) (\\S+) (\\S+) (\\S+) \"([^\"]+)\" \"(.+?)\" (\\S+) (\\S+) (\\S+) (\\S+) (\\S+) (\\S+) (\\S+)'
) LOCATION 's3://BUCKET_NAME/PREFIX/'
TBLPROPERTIES ('has_encrypted_data'='false');

検索クエリ

SELECT * FROM "default"."accesslog" WHERE signature_version = 'SigV2' limit 100;

Athenaはデータスキャンのサイズによる課金になるのと、何もしないとLOCATIONで指定したプレフィクスに一致するファイルが全部対象になるので、どれだけのデータを読み込むことになるか、というのはちゃんと見積もったほうが良いです。

パーティションという仕組みを使うと、条件に合致するLOCATIONのみを対象にできますが、パーティションを使えるかどうかはキーのフォーマットに依存します。
S3が出力するアクセスログのキーのフォーマットの都合上、パーティションに当たる情報が作ることが出来ません(多分...)

なので、もしアクセス頻度が多いバケットを対象とする場合、この方法だと少し不安です。パーティションは使いたい。

CloudTrail + Athena

前の方法ではパーティションが作れない、バケットが1つならいいが複数バケット扱うとなるとその数だけ設定が必要、という点が辛みとしてあげられます。

CloudTrailを使うと、S3の操作を受けたらCloudTrailのログとして記録することができ、また複数のS3バケットの操作ログをCloudTrailのS3バケットへ集約することもできます。これに対してAthenaで検索をかけようという魂胆です。

しかし、CloudTrailが記録するデータにはちょっとクセがあり、JSONデシリアライザではうまく定義をかけませんでした。Recordsの1カラムになってしまう感じで。
調べていたところ、「Athena で S3 署名バージョン2の利用状況レポートを作成する」のやり方も良さそうだ、と思っていましたが、どうやらCloudTrail用のデシリアライザがあり、JSONデシリアライザを使わずとも良い感じに書けるようです。

ちなみに、これを使った方法で作るだけならCloudTrailの画面からポチポチすれば作れます。詳しくはクラスメソッドさんのBlogをお読みください。

ただこの場合、パーティションは作られません。
幸い、画面上からの作成時に定義をチラ見せしてくれるので、これにパーティションを足した形にすることで無理やり足します。

S3へ記録するときのKeyがregion/yyyy/mm/dd/...という形になっているので、地域・日別のパーティションを作ることができます。

Athenaのテーブル定義。テーブル名やs3://BUCKET_NAME/AWSLogs/XXXXXXXXXXXXXX/CloudTrail/はいい感じに置き換えてください。

CREATE EXTERNAL TABLE cloudtrail_logs_BUCKETNAME (
    eventVersion STRING,
    userIdentity STRUCT<
        type: STRING,
        principalId: STRING,
        arn: STRING,
        accountId: STRING,
        invokedBy: STRING,
        accessKeyId: STRING,
        userName: STRING,
        sessionContext: STRUCT<
            attributes: STRUCT<
                mfaAuthenticated: STRING,
                creationDate: STRING>,
            sessionIssuer: STRUCT<
                type: STRING,
                principalId: STRING,
                arn: STRING,
                accountId: STRING,
                userName: STRING>>>,
    eventTime STRING,
    eventSource STRING,
    eventName STRING,
    awsRegion STRING,
    sourceIpAddress STRING,
    userAgent STRING,
    errorCode STRING,
    errorMessage STRING,
    requestParameters STRING,
    responseElements STRING,
    additionalEventData STRING,
    requestId STRING,
    eventId STRING,
    resources ARRAY<STRUCT<
        arn: STRING,
        accountId: STRING,
        type: STRING>>,
    eventType STRING,
    apiVersion STRING,
    readOnly STRING,
    recipientAccountId STRING,
    serviceEventDetails STRING,
    sharedEventID STRING,
    vpcEndpointId STRING
) PARTITIONED BY (
  `region` STRING,
  `dt` STRING
)
ROW FORMAT SERDE 'com.amazon.emr.hive.serde.CloudTrailSerde'
STORED AS INPUTFORMAT 'com.amazon.emr.cloudtrail.CloudTrailInputFormat'
OUTPUTFORMAT 'org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat'
LOCATION 's3://BUCKET_NAME/AWSLogs/XXXXXXXXXXXXXX/CloudTrail/'
TBLPROPERTIES ('classification'='cloudtrail');

ap-northeast-1の日別毎にPartitionを追加(4/18-4/20の3日分)
都度ALTER文で作る必要があります。この辺は自動化ポイントですね・・・
また、パーティションは上限数が決まっているので注意します。

ALTER TABLE default.cloudtrail_logs_BUCKETNAME ADD PARTITION (`region`='ap-northeast-1', `dt`='2019/04/18') location 's3://BUCKET_NAME/AWSLogs/XXXXXXXXXXXXXX/CloudTrail//ap-northeast-1/2019/04/18/';
ALTER TABLE default.cloudtrail_logs_BUCKETNAME ADD PARTITION (`region`='ap-northeast-1', `dt`='2019/04/19') location 's3://BUCKET_NAME/AWSLogs/XXXXXXXXXXXXXX/CloudTrail//ap-northeast-1/2019/04/19/';
ALTER TABLE default.cloudtrail_logs_BUCKETNAME ADD PARTITION (`region`='ap-northeast-1', `dt`='2019/04/20') location 's3://BUCKET_NAME/AWSLogs/XXXXXXXXXXXXXX/CloudTrail//ap-northeast-1/2019/04/20/';

検索クエリの例。WHEREでregionとdtを指定することで、条件に一致するパーティションのロケーションを対象になることで絞られる、という仕組みのようです。

SELECT * FROM (
  SELECT *, json_extract_scalar(additionaleventdata , '$.SignatureVersion') AS signature_version FROM "default"."cloudtrail_logs_BUCKETNAME"
  WHERE 
  region = 'ap-northeast-1'
  AND dt BETWEEN '2019/04/17' AND '2019/04/19'
  -- AND signature_version = 'SigV2' -- なぜか `Column 'signature_version' cannot be resolved` と言われる
)
WHERE
  signature_version = 'SigV2'
LIMIT 10;

ちょっとエラーになる原因がわからず、ワークアラウンドとしてサブクエリにして外側で絞るという効率の悪そうな事をやっている。
でもデータスキャンのサイズは変わらないし、誤差だよ誤差!

Athenaメモ

Athenaは裏でGlueを使う。なので、テーブルを作る場合、Glueにも権限を与えないとAthena上でCreate Tableのクエリ実行時に失敗する。

チューニングについて。データスキャンや実行時間を減らす際に読む。
https://aws.amazon.com/jp/blogs/big-data/top-10-performance-tuning-tips-for-amazon-athena/

制限事項について。パーティション数やクエリの長さなど。
https://docs.aws.amazon.com/ja_jp/athena/latest/ug/service-limits.html

意図せず大量のデータスキャンを発生させても、事故らないようにする仕組みもある模様。今回はそこまでならんだろう、ということで様子見状態。
https://docs.aws.amazon.com/ja_jp/athena/latest/ug/manage-queries-control-costs-with-workgroups.html

クラウド難しい。