Techfirm Cloud Architect Blog

テックファーム株式会社クラウドインフラグループのブログ

S3に出力したログをAmazon Athenaでクエリする

はじめに

ウェブアプリケーションの運用で、ログを解析するとき、
解析専用サーバを構築したり、有料の解析ソフトを使うケースが一般的だと思うが、
生ログをエクセルなどで解析するケースも稀ではないように思う。
たとえば、ALBのアクセスログはS3に保存することができるが、
ファイルは5分毎に分割され、gz形式で圧縮されているため、ログの中身を見るには多少手間がかかる。
Amazon Athenaでは、S3のログを、分割され圧縮された状態でも、
とくにそういったファイルの状態を意識することなくデータの抽出を行うことができる。
そこで本稿では、S3に出力されたログに対し、Amazon Athenaを使ってクエリを実行する方法を紹介する。

Amazon Athenaとは

S3内のデータを、SQLクエリを使って直接抽出できる。
サーバレスにクエリを実行でき、Apache Sparkと併用することも可能。
大規模データでも高速な処理が可能で、料金はクエリ実行時にスキャンしたデータ量に対してのみ発生。
東京リージョンでは、1TBあたり5.00ドルが課金される。

Amazon Athenaを利用するには

AWSマネジメントコンソールで、Amazon Athenaを開く。
クエリエディタを開き、まずはデータベースおよびテーブルの作成を行う。

クエリ結果の保存場所を作成

S3に、クエリの保存先用の場所を作成する。
クエリエディタの、[設定] 、 [管理]から設定する。

データベースの作成

Amazon Athenaでは、データベースを定義することができる。
データベースを分けておくと、それぞれのデータベースに同じ名前のテーブルを作成できるため、
クエリを使いまわすときなどは便利だと思うが、作成しなくても特に問題はない。
データベースを作成する場合は、クエリエディタから下記の様なCREATE文を実行する。

CREATE (DATABASE|SCHEMA) [IF NOT EXISTS] database_name

テーブルの作成

クエリエディタから次のようなDDLステートメントを実行する。
下記は、パーティション射影(Partition Projection)を使用したテーブル作成の例文。
パーティション射影によってパーティション管理を自動化することができる。

CREATE EXTERNAL TABLE IF NOT EXISTS alb_logs (
            type string,
            time string,
            elb string,
            client_ip string,
            client_port int,
            target_ip string,
            target_port int,
            request_processing_time double,
            target_processing_time double,
            response_processing_time double,
            elb_status_code int,
            target_status_code string,
            received_bytes bigint,
            sent_bytes bigint,
            request_verb string,
            request_url string,
            request_proto string,
            user_agent string,
            ssl_cipher string,
            ssl_protocol string,
            target_group_arn string,
            trace_id string,
            domain_name string,
            chosen_cert_arn string,
            matched_rule_priority string,
            request_creation_time string,
            actions_executed string,
            redirect_url string,
            lambda_error_reason string,
            target_port_list string,
            target_status_code_list string,
            classification string,
            classification_reason string
            )
            PARTITIONED BY
            (
             day STRING
            )
            ROW FORMAT SERDE 'org.apache.hadoop.hive.serde2.RegexSerDe'
            WITH SERDEPROPERTIES (
            'serialization.format' = '1',
            'input.regex' = 
        '([^ ]*) ([^ ]*) ([^ ]*) ([^ ]*):([0-9]*) ([^ ]*)[:-]([0-9]*) ([-.0-9]*) ([-.0-9]*) ([-.0-9]*) (|[-0-9]*) (-|[-0-9]*) ([-0-9]*) ([-0-9]*) \"([^ ]*) (.*) (- |[^ ]*)\" \"([^\"]*)\" ([A-Z0-9-_]+) ([A-Za-z0-9.-]*) ([^ ]*) \"([^\"]*)\" \"([^\"]*)\" \"([^\"]*)\" ([-.0-9]*) ([^ ]*) \"([^\"]*)\" \"([^\"]*)\" \"([^ ]*)\" \"([^\s]+?)\" \"([^\s]+)\" \"([^ ]*)\" \"([^ ]*)\"')
            LOCATION 's3://your-alb-logs-directory/AWSLogs/<ACCOUNT-ID>/elasticloadbalancing/<REGION>/'
            TBLPROPERTIES
            (
             "projection.enabled" = "true",
             "projection.day.type" = "date",
             "projection.day.range" = "2022/01/01,NOW",
             "projection.day.format" = "yyyy/MM/dd",
             "projection.day.interval" = "1",
             "projection.day.interval.unit" = "DAYS",
             "storage.location.template" = "s3://your-alb-logs-directory/AWSLogs/<ACCOUNT-ID>/elasticloadbalancing/<REGION>/${day}"
            )

パーティション射影を使用するには、テーブルプロパティでパーティションキーのルールやフォーマットを指定する。
上の例では、projection.day.rangeで対象とするデータの期間を2022年1月1日から現在まで指定し、
strage.location.templateで、S3のログファイルの場所を指定している。
また、データベースを指定したい場合は、クエリエディタ付近にあるプルダウンから
任意のデータベースを指定するか、下記の様にデータベース名を指定してもよい。

CREATE EXTERNAL TABLE IF NOT EXISTS database_name.alb_logs

テーブル作成のDDLステートメントに関する詳細はAWS公式ドキュメントから確認できる。
パーティション射影を使用した Athena での ALB ログ用のテーブルの作成

パーティションを使用するメリット

パーティション化することで、下記2点に対してメリットを得られる。

  • 検索速度
  • コスト
検索速度

パーティションを指定しない場合、クエリを実行する際はすべてのデータが検索対象となる。
対象データが多いと、検索結果を得られるまで時間がかかる場合がある。

コスト

前項で述べた通りテーブルをパーティション化すると、クエリ実行時のスキャン対象データを限定できるようになる。
Amazon Athenaの料金はスキャン対象のデータ量に対して課金されるため、
スキャン対象のデータが少なければ当然請求金額も少なくなり、コスト面でのメリットになる。

クエリを実行してみる

それではここでいくつかのクエリを紹介してみたいと思う。

日時の範囲指定

まずは、WHERE句で、抽出したいログの時間帯を範囲指定するクエリを紹介する。

日付型への変換とタイムゾーンの変更

LBのログに出力される日時は、タイムゾーンがUTCで、文字列型であるため、
WHERE句で時間や日時を指定するには、parse_datetimeを使用しdate型に変換する。
また、WHERE句で日時指定する際に日本時間を使用したい場合は、タイムゾーンにAsia/Tokyoを指定する。
下記は、日本時間2023年1月1日から1月10日までを指定するクエリ。

SELECT
 parse_datetime(time,'yyyy-MM-dd''T''HH:mm:ss.SSSSSS''Z') AT TIME ZONE 'Asia/Tokyo' as time,
 request_verb,
 request_url,
 client_ip,
 elb_status_code,
 target_status_code,
 target_processing_time,
 response_processing_time,
 user_agent
FROM alb_logs
WHERE parse_datetime(time,'yyyy-MM-dd''T''HH:mm:ss.SSSSSS''Z') AT TIME ZONE 'Asia/Tokyo'
 BETWEEN timestamp '2023-01-01 00:00 Asia/Tokyo' AND timestamp '2023-01-10 23:59 Asia/Tokyo'
ORDER BY time;

1時間ごとのアクセス数を集計

次に紹介するのは時間毎のアクセス数を集計するクエリ。

substrでtimeのYYYY-MM-DDTHH部分を抽出

substr関数を使用し、アクセス日時の先頭から13文字を抽出する。
同じ時刻のデータをcountし、1時間当たりのアクセス数を集計する。
下記例では、2023年1月1日から1月11日までの、ステータスコードが504および460の、1時間ごとのアクセス数を集計する。

SELECT substr(time,1,13) AS time_every_hour, count(target_status_code)
FROM alb_logs
WHERE parse_datetime(time,'yyyy-MM-dd''T''HH:mm:ss.SSSSSS''Z') AT TIME ZONE 'Asia/Tokyo'
 BETWEEN timestamp '2023-01-01 00:00 Asia/Tokyo' AND timestamp '2023-01-11 23:59 Asia/Tokyo'
 AND (elb_status_code = 504 OR elb_status_code = 460)
group BY substr(time,1,13)
order by time_every_hour

array型のカラムをWHERE句やSELECTで指定

ALBのログでは該当しないが、WAFのログの様にarrayデータ型のカラムから部分的にデータを取得したり、
WHERE句でarrayデータ型のカラムを条件指定の対象に使用したいときは、UNNEST演算子を使用する。

配列型の要素を1行のデータに変換

例えばWAFのログのlabelsは、下記の様な内容になっている。

"labels":[{"name":"awswaf:managed:token:absent"},{"name":"awswaf:managed:aws:bot-control:bot:verified"},{"name":"awswaf:managed:aws:bot-control:bot:category:search_engine"},{"name":"awswaf:managed:aws:bot-control:bot:name:googlebot"}]

下記は、任意のWAFルールに該当したリクエストをWHERE句で指定し抽出するクエリで、
labelsをネストし、labelsのnameをWHERE句で指定できるようにしている。
事前に、テーブル名が「waf_logs」というAWS WAFログのテーブルを作成しているものとする。

SELECT from_unixtime(timestamp / 1000, 'Asia/Tokyo') as jst, action, terminatingrulematchdetails, httprequest, label.name
FROM waf_logs, UNNEST(labels) t(label)
WHERE from_unixtime(timestamp / 1000, 'Asia/Tokyo')
 BETWEEN timestamp '2023-01-01 00:00' AND timestamp '2023-01-31 23:59'
AND (label.name LIKE '%HostingProviderIPList%' OR label.name LIKE '%Custom_MicrosoftAds_NotMicrosoftAds%')
ORDER BY timestamp;

さいごに

ファイルの中身をダイレクトにクエリできるAmazon Athenaは、とても便利で使い勝手がいいサービスだと思う。
対象の複数ファイルをダウンロードしたり、解凍したりする必要がなく、
慣れ親しんだSQLでクエリできるため、S3にあるファイルでも時間をかけずに解析できる。
利用したことがない人はぜひ試してみてほしい。