S3 サーバーアクセスログを Athena + シンボリックリンクを利用して分析してみた
アノテーション、テクニカルサポートチームの村上です。
Athena を利用して S3 上のログを分析したいというお問い合わせいただくことがあります。
S3 サーバーアクセスログのようにフォルダが作成されずログファイルがバケット内にフラットに配置されるログの場合は、クエリの対象となるデータのスキャン範囲がバケット全体となってしまう為に、クエリ時間が長くなる、データスキャンに対する課金額が大きくなるといった課題があります。
Athena ではパーティションという概念があり、パーティションを指定することでスキャン対象のデータを絞ることができクエリの実行時間とサービス利用料(スキャンしたデータに対しての従量課金)を削減することができます。
以上のような理由があり、バケット内にフラットに配置されたログについてのお問い合わせがあると、下記ブログも併せてご案内していました。
ご案内している以上、一度は自分でも実装しておかないとというのが本記事執筆のモチベーションとなりました。実際に実装してみると、「アレ?」と悩むポイントがいくつかあったので、私なりに嚙み砕いた内容でお伝えしますので、ぜひお付き合いください。
Athena パーティションとは?
普段使いの言葉で言うとフォルダのようなものでしょうか。Athena でクエリを実行する際に、あるフォルダ配下にあるデータのみをクエリの対象データとして指定することができます。S3 と Athena において適切なパーティション設定がされている場合、クエリの WHERE 句で WHERE dt=2023-07-02 等と指定して S3 内の全データのうち特定のデータのみスキャンの対象とすることができます。
シンボリックリンクとは?
シンボリックリンク 【symbolic link】 symlink
シンボリックリンクとは、オペレーティングシステム(OS)のファイルシステムの機能の一つで、特定のファイルやディレクトリを指し示す別のファイルを作成し、それを通じて本体を参照できるようにする仕組み。
Windows だと、ショートカットにあたります。
ディスクトップ上に配置しておいたショートカットをダブルクリックするとリンクが設定してあるファイルに跳ぶことができますが、ショートカットを消してもリンクが消えるだけでリンクのファイルには影響がありません。
実現したいこと
本記事では、ログファイルが存在する S3(バケット 1 )とは別の S3 (バケット 2 )上にシンボリックリンクを作成して、バケット 1 に存在する全ログファイルの内の一部のログファイルのみをデータスキャン対象とする方法をご紹介します。
Athena からクエリを実行する際に Loacation としてはバケット 2 を指定するのですが、Athena がバケット 2 の指定したパーティションを読み込みにいくと、オブジェクト URL が記述されたテキストファイル(マニフェストファイル)が格納されており、その URL に従ってバケット 1 内のオブジェクトを読み込みにいくようなイメージとなります。
やってみた
下準備
S3 のサーバーアクセスのログ記録を有効にした S3 バケット
test-bucket20230702
S3 サーバーアクセスのログ記録が配信されるターゲットバケット(バケット 1 )
s3-accesslog-source-bucket
シンボリックリンクを設定する S3 バケット(バケット 2 )
bucket-for-athena-symboliclink-query
S3(s3-accesslog-source-bucket バケット 1 )内に配置されている S3 サーバーアクセスのログファイル
※ 実際には、2023-07-02 以外にも、2023-07-06、2023-07-07、2023-07-08、2023-08-11 についてのサーバーアクセスログが配置してあります。
$ aws s3 ls s3://s3-accesslog-source-bucket/ 2023-07-02 14:12:45 677 2023-07-02-05-12-44-EB21F0A3B3CDD1EF 2023-07-02 14:14:57 1376 2023-07-02-05-14-56-B7288123B8BDF900 2023-07-02 14:15:26 509 2023-07-02-05-15-25-E8D85A5E5ACDBD78 2023-07-02 14:17:52 677 2023-07-02-05-17-51-BF8C47DE12611DB5 2023-07-02 14:18:33 693 2023-07-02-05-18-32-F94F74FF881870E8 2023-07-02 14:18:37 678 2023-07-02-05-18-36-ECC55D4A154829CC 2023-07-02 14:18:50 747 2023-07-02-05-18-49-7FE07D47A43CB8BA 2023-07-02 14:19:23 677 2023-07-02-05-19-22-F4780C55164E285A 2023-07-02 14:21:32 693 2023-07-02-05-21-31-2262A829D5A5C9BA 2023-07-02 14:21:43 728 2023-07-02-05-21-42-AAECB13C180BC7A1 2023-07-02 14:21:53 682 2023-07-02-05-21-52-0614D40B68E86295 2023-07-02 14:22:01 686 2023-07-02-05-22-00-DE584B588E1ED953 2023-07-02 14:22:40 512 2023-07-02-05-22-39-E923759804A062B0 2023-07-02 14:22:46 748 2023-07-02-05-22-45-ED828BB1D159CFF5 2023-07-02 14:23:41 699 2023-07-02-05-23-40-0D8A5784109EF7D5 2023-07-02 14:25:20 728 2023-07-02-05-25-19-52C5486F963ADD9E 2023-07-02 14:25:59 677 2023-07-02-05-25-58-BF6151D2C4898D81 2023-07-02 14:26:17 416 2023-07-02-05-26-16-A523E076B85356D4 2023-07-02 14:26:30 693 2023-07-02-05-26-29-2054FF7E47C36D65 2023-07-02 14:27:13 512 2023-07-02-05-27-12-BD44F9F1E1C2FE42 2023-07-02 14:27:47 522 2023-07-02-05-27-46-DC45D72E301EE620 2023-07-02 14:28:08 8534 2023-07-02-05-28-07-14AF6E0EC9ABD460 2023-07-02 14:28:23 414 2023-07-02-05-28-21-954749608DAACB80 2023-07-02 14:30:17 1405 2023-07-02-05-30-16-3378BFF81B9DC05A 2023-07-02 14:30:24 8584 2023-07-02-05-30-23-2F64CB302AD85982 2023-07-02 14:30:39 449 2023-07-02-05-30-38-637DA1BF95BBCE42 2023-07-02 14:31:43 1373 2023-07-02-05-31-42-5738117A15DDB8BE 2023-07-02 14:32:01 8288 2023-07-02-05-32-00-A6771930B857B8B9 2023-07-02 14:32:37 693 2023-07-02-05-32-36-90D9A9321ACA734A 2023-07-02 14:32:54 728 2023-07-02-05-32-53-3D99E291BC4EBF76 2023-07-02 14:33:24 677 2023-07-02-05-33-23-6C179A01B8BCB0C2 2023-07-02 14:34:24 2127 2023-07-02-05-34-23-22FA4380C2CB1619 2023-07-02 14:35:41 677 2023-07-02-05-35-40-0F9DE86D27272CEF 2023-07-02 14:36:02 8763 2023-07-02-05-36-01-486832ADE22D5055 2023-07-02 14:36:50 677 2023-07-02-05-36-49-370BC142DEC62B0D
パーティションが設定されてない状態で Athena からクエリを実行してみる
S3 サーバーアクセスログに対してクエリを実行する為に、Athena テーブルを作成します。Athena テーブル作成にあたり、re:Post 記載の手順をそのまま実行しました。
Athena を使用して Amazon S3 サーバーアクセスログを分析する方法を教えてください。
create database s3_access_logs_db
CREATE EXTERNAL TABLE `s3_access_logs_db.mybucket_logs`( `bucketowner` STRING, `bucket_name` STRING, `requestdatetime` STRING, `remoteip` STRING, `requester` STRING, `requestid` STRING, `operation` STRING, `key` STRING, `request_uri` STRING, `httpstatus` STRING, `errorcode` STRING, `bytessent` BIGINT, `objectsize` BIGINT, `totaltime` STRING, `turnaroundtime` STRING, `referrer` STRING, `useragent` STRING, `versionid` STRING, `hostid` STRING, `sigv` STRING, `ciphersuite` STRING, `authtype` STRING, `endpoint` STRING, `tlsversion` STRING) ROW FORMAT SERDE 'org.apache.hadoop.hive.serde2.RegexSerDe' WITH SERDEPROPERTIES ( 'input.regex'='([^ ]*) ([^ ]*) \\[(.*?)\\] ([^ ]*) ([^ ]*) ([^ ]*) ([^ ]*) ([^ ]*) (\"[^\"]*\"|-) (-|[0-9]*) ([^ ]*) ([^ ]*) ([^ ]*) ([^ ]*) ([^ ]*) ([^ ]*) (\"[^\"]*\"|-) ([^ ]*)(?: ([^ ]*) ([^ ]*) ([^ ]*) ([^ ]*) ([^ ]*) ([^ ]*))?.*$') STORED AS INPUTFORMAT 'org.apache.hadoop.mapred.TextInputFormat' OUTPUTFORMAT 'org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat' LOCATION 's3://s3-accesslog-source-bucket/'
試しに、2 つのクエリを実行してみましたが、データを区切る仕切り(パーティション)がない為、どのようなクエリを実行しても S3 内のデータがフルスキャンされてからクエリが実行されます。
S3(バケット 2 )上に Apache Hive 形式でフォルダを作成する
bucket-for-athena-symboliclink-query(バケット 2 )において、dt=2023-07-02 のように Hive 形式でフォルダを作成します。フォルダの名前が、dt=2023-07-02 のようにキーと値で作成されているので、Athena からのクエリ実行時に WHERE 句において dt=2023-07-02 や dt=2023-08-11 のようにパーティションを指定したデータスキャンが可能となります。
Athena では Apache Hive スタイルのパーティションを使用できます。このパーティションのデータパスには、等号で連結されたキーと値のペア (例えば country=us/... または year=2021/month=01/day=26/...) が含まれています。つまり、それぞれのパスにより、パーティションのキーと値、両方の名前が表されます。
bucket-for-athena-symboliclink-query(バケット 2 )上に以下のようにフォルダを作成しました。
※ year=2023/month=07/day=02 のようなフォルダ構成でパーティションを作成することも可能ですが、本記事ではシンプルな構成にしています。
S3(バケット 2 )上のフォルダにシンボリックリンクで利用するマニフェストファイルを配置する
S3(バケット 1 )上のログファイルのオブジェクト URL を日付別に取得して、マニフェストファイルに記述します。日付別でのオブジェクト URL の取得は、以下の手順で実行しました。
1. AWS CLI (s3 ls) コマンドの出力結果に対して、awk コマンドで指定したフィールドの値(オブジェクト URL)のみを取得してからテキストファイルへリダイレクト
aws s3 ls s3://s3-accesslog-source-bucket --recursive | awk '{print "s3://s3-accesslog-source-bucket/"$4}' > locations.txt
2. 1.で作成したファイルに対して日付ごとに grep して、日付別のオブジェクト URL が記述されたマニフェストファイルを作成
cat locations.txt | grep 2023-07-02 > 2023-07-02.txt
以下のようなマニフェストファイルを作成しました。
$ cat 2023-07-02.txt s3://s3-accesslog-source-bucket/2023-07-02-05-12-44-EB21F0A3B3CDD1EF s3://s3-accesslog-source-bucket/2023-07-02-05-14-56-B7288123B8BDF900 s3://s3-accesslog-source-bucket/2023-07-02-05-15-25-E8D85A5E5ACDBD78 s3://s3-accesslog-source-bucket/2023-07-02-05-17-51-BF8C47DE12611DB5 s3://s3-accesslog-source-bucket/2023-07-02-05-18-32-F94F74FF881870E8 s3://s3-accesslog-source-bucket/2023-07-02-05-18-36-ECC55D4A154829CC s3://s3-accesslog-source-bucket/2023-07-02-05-18-49-7FE07D47A43CB8BA s3://s3-accesslog-source-bucket/2023-07-02-05-19-22-F4780C55164E285A s3://s3-accesslog-source-bucket/2023-07-02-05-21-31-2262A829D5A5C9BA s3://s3-accesslog-source-bucket/2023-07-02-05-21-42-AAECB13C180BC7A1 s3://s3-accesslog-source-bucket/2023-07-02-05-21-52-0614D40B68E86295 s3://s3-accesslog-source-bucket/2023-07-02-05-22-00-DE584B588E1ED953 s3://s3-accesslog-source-bucket/2023-07-02-05-22-39-E923759804A062B0 s3://s3-accesslog-source-bucket/2023-07-02-05-22-45-ED828BB1D159CFF5 s3://s3-accesslog-source-bucket/2023-07-02-05-23-40-0D8A5784109EF7D5 s3://s3-accesslog-source-bucket/2023-07-02-05-25-19-52C5486F963ADD9E s3://s3-accesslog-source-bucket/2023-07-02-05-25-58-BF6151D2C4898D81 s3://s3-accesslog-source-bucket/2023-07-02-05-26-16-A523E076B85356D4 s3://s3-accesslog-source-bucket/2023-07-02-05-26-29-2054FF7E47C36D65 s3://s3-accesslog-source-bucket/2023-07-02-05-27-12-BD44F9F1E1C2FE42 s3://s3-accesslog-source-bucket/2023-07-02-05-27-46-DC45D72E301EE620
上記手順を考えるのに苦労しました。もっとスマートな方法もあるかと思いますが、今回はこの手順で日付ごとのマニフェストファイルを作成していきます。
日付ごとに作成したマニフェストファイル(2023-07-02.txt)を、対応するフォルダ(dt=2023-07-02)へ以下のように配置しました。
$ aws s3 ls s3://bucket-for-athena-symboliclink-query --recursive 2023-08-13 11:15:23 0 dt=2023-07-02/ 2023-08-13 11:16:55 8678 dt=2023-07-02/2023-07-02.txt 2023-08-13 11:15:36 0 dt=2023-07-06/ 2023-08-13 11:17:10 1932 dt=2023-07-06/2023-07-06.txt 2023-08-13 11:15:51 0 dt=2023-07-07/ 2023-08-13 11:17:23 414 dt=2023-07-07/2023-07-07.txt 2023-08-13 11:16:03 0 dt=2023-07-08/ 2023-08-13 11:17:38 759 dt=2023-07-08/2023-07-08.txt 2023-08-13 11:16:17 0 dt=2023-08-11/ 2023-08-13 11:17:50 828 dt=2023-08-11/2023-08-11.txt
パーティション化された Athena テーブルを作成する
パーティションとして dt を追加した mybucket_logs_with_partition テーブルを作成します。
CREATE EXTERNAL TABLE `s3_access_logs_db.mybucket_logs_with_partition`( `bucketowner` STRING, `bucket_name` STRING, `requestdatetime` STRING, `remoteip` STRING, `requester` STRING, `requestid` STRING, `operation` STRING, `key` STRING, `request_uri` STRING, `httpstatus` STRING, `errorcode` STRING, `bytessent` BIGINT, `objectsize` BIGINT, `totaltime` STRING, `turnaroundtime` STRING, `referrer` STRING, `useragent` STRING, `versionid` STRING, `hostid` STRING, `sigv` STRING, `ciphersuite` STRING, `authtype` STRING, `endpoint` STRING, `tlsversion` STRING ) PARTITIONED BY (dt string) ROW FORMAT SERDE 'org.apache.hadoop.hive.serde2.RegexSerDe' WITH SERDEPROPERTIES ( 'input.regex'='([^ ]*) ([^ ]*) \\[(.*?)\\] ([^ ]*) ([^ ]*) ([^ ]*) ([^ ]*) ([^ ]*) (\"[^\"]*\"|-) (-|[0-9]*) ([^ ]*) ([^ ]*) ([^ ]*) ([^ ]*) ([^ ]*) ([^ ]*) (\"[^\"]*\"|-) ([^ ]*)(?: ([^ ]*) ([^ ]*) ([^ ]*) ([^ ]*) ([^ ]*) ([^ ]*))?.*$') STORED AS INPUTFORMAT 'org.apache.hadoop.hive.ql.io.SymlinkTextInputFormat' OUTPUTFORMAT 'org.apache.hadoop.hive.ql.io.IgnoreKeyTextOutputFormat' LOCATION 's3://bucket-for-athena-symboliclink-query/'
Athena クエリエディタで MSCK REPAIR TABLE コマンドを実行し、パーティションをロードします。
※ 既に dt=2023-07-02 のパーティションがある状態で他の全パーティションを追加した結果が表示されています。
パーティションを指定してクエリを実行してみた
意図したようにパーティションの追加ができたようなので、パーティション dt=2023-07-02 を指定してクエリを実行してみます。
パーティション dt=2023-07-02 を指定してクエリを実行しているので、スキャンしたデータ:142.67 KB となり、パーティションが無い状態でクエリを実行した場合(208.90 KB)と比べて 66 KB 程スキャン量を削減することができました。
他日付のパーティションも有効となっているか確認してみます。パーティション dt=2023-08-11 を指定してクエリを実行してみます。
2023-08-11 は、S3 サーバーアクセスログのファイル数が少ない為、15.02 KB のデータスキャン量でクエリを実行することができました。
ここで注意深い読者の方だと、バケット 1 に対してデータを全件取得した場合と比べて、バケット 2 上のパーティションを指定したデータ全件取得の方がクエリ実行時間が長くなっていることに気がついたかと思います。あくまで推測ですが、シンボリックリンクを利用すると個々のオブジェクト URL に対して Athena がデータを読み込みに行く必要があり、その処理の累積による結果と考えられます。
運用面から S3 に保存されたログへのシンボリックリンクの作成と利用について考えてみる
結論から言うと、特定日付のログのみを重点的に調べたいような場合は、シンボリックリンクを作成・利用してみてもいいかと思います。ただし、シンボリックリンク作成の手間を考えると以下の re:Post に記載があるように S3 のライフサイクルポリシーでスキャンするデータ量を削減するだけでいいように思いました。ログをずっと S3 標準においておく必要もないのですから。
Athena を使用して Amazon S3 サーバーアクセスログを分析する方法を教えてください。
サーバーアクセスログのバケット用のライフサイクルポリシーを作成するのがベストプラクティスです。ログファイルを定期的に削除するようにライフサイクルポリシーを設定できます。これにより、各クエリで Athena が分析するデータの量を減らすことができます。
まとめ
先人のブログでは、シンボリックリンクを作成するオブジェクト数が多い場合のマニフェストファイル作成方法への言及が無かったので、そこを補完できるブログになったかなと思います。手を動かしてブログ化したことで、自分の中でもいろいろと腹落ちしました。
この記事がどなたかのお役に立てば幸いです。