今すぐ ALB のアクセスログをクエリする

クックパッドマートでサーバーサイドなどのソフトウェアエンジニアをしている石川です。

この記事では、クックパッドマートとは全然関係なく、私が正社員として新卒入社する前、2020 年初頭に技術部 SRE グループで就業型インターンをしていた際に実装したシステムについてご紹介します。

ALB のアクセスログ

弊社では AWS の Elastic Load Balancing (ELB) を多用しており、Application Load Balancer (ALB) が多くのウェブアプリケーションで利用されています。

ところで ALB はアクセスログを Amazon S3保存することができます。このアクセスログにはアクセス先の IP アドレスやリクエスト URL の他、レスポンスのステータスコード、レスポンスまでにかかった時間、User-Agent などの情報が記録されています。

これらの情報は分析の役に立ちます。ロードバランサーは典型的にはユーザーからのアクセスが最初に到達する場所であるため、たとえばエラーとなったアクセスがどこでエラーになったかの調査に ALB のアクセスログが役に立つ場合があります。

また、他にも社内だと以下の用途で ALB のアクセスログが使われたことがありました。

  • 特定のエンドポイントのレスポンスタイム分布の分析
  • 特定の User-Agent を持つリクエスト数の調査
  • 負荷試験のアクセスパターンを作る際の参考データ
  • 非推奨にしたエンドポイントへのアクセス数の調査
  • 499 Client Closed Request の頻度調査

(ただし、ALB のアクセスログはベストエフォートで記録されていることには注意が必要です。ドキュメントにも “We recommend that you use access logs to understand the nature of the requests, not as a complete accounting of all requests.” と書かれています。)

Athena を使ったクエリ

では実際にアクセスログを分析したいとなったとき、どうすれば良いでしょうか。ログが記録されたテキストファイルをダウンロードして手元でスクリプトを実行することでも分析できますが、このようなやり方を毎回繰り返すのは煩わしいですし、素朴な実装だと実行時間もそれなりにかかります。

そこで、ここでは Amazon Athena を使う方法について考えます。Athena は S3 に置かれた大量のデータファイル群に対して SQL で柔軟かつ高速にクエリができるサービスです。S3 に置かれている大量の ALB ログを素早く分析したいという今回のケースにはぴったりです。

実際、AWS のドキュメントには Athena で ALB ログをクエリするためにテーブルを作る SQL のサンプルが書かれています: https://docs.aws.amazon.com/athena/latest/ug/application-load-balancer-logs.html

このようにテーブルを作っておくと、たとえば「GET /users の最近のレスポンスタイムの平均」をクエリしたり「POST /a/deprecated/endpoint に最近目立ったアクセスがあるかどうか」をクエリしたりでき、便利です。具体的には以下のような SQL を書くことになります *1

select
    count(*) as log_count
    , date(from_iso8601_timestamp(logs.time)) as log_date
from
    alb_access_logs.cookpad as logs
where
    date(from_iso8601_timestamp(logs.time)) >= date '2021-09-01'
    and logs.request_verb = 'POST'
    and logs.request_url like '%/a/deprecated/endpoint'
group by 2
order by 2

注意点として、Athena の料金はクエリ時にスキャンされたデータサイズについての従量課金制です。不必要に過去のログすべてに渡ってスキャンすると無駄に課金されてしまいます。クエリにかかる時間を考えても無駄です。

そこでパーティションを利用します。ALB ログが保存されている S3 key には year/month/day が含まれているので、ここについてパーティションを作り、クエリ時に year/month/day について絞り込むことでスキャンサイズを落とすことができます。*2

とはいえ、ALB ログを分析したくなる度にその ALB について Athena でテーブルを作ってパーティションを作って……とするのは面倒です。あらかじめ作ってあった方が便利ですし、日常的な調査をより機敏に行うことができるでしょう。

ということで ALB ログをいつでも Athena で分析できるようにこのあたりを自動化しよう、というのが、私がインターンで取り組んだタスクでした。

テーブルおよびパーティション作成の自動化

さて、それでは自動化いたしましょう。やることは単純で「まだテーブルが作られていない ALB についてテーブルを作り、まだ作られていないパーティションについてパーティションを作る」というバッチを実装すれば良いです *3。今回パーティションは year/month/day 単位で付けようとしているため、実装したバッチは日次で実行すれば良いでしょう。

実装について考えます。テーブル作成部分については Athena で行う方法を先述しましたが、今回のバッチでは Athena の API を使うのではなく、Athena と統合して利用できるAWS Glue の API を使うことにしました。これは、生の SQL を実行することになる Athena よりも「テーブルを作る」などの操作ごとに API が用意されている Glue の方がより細かく権限管理できるためです。また今回の使い方だと Glue の利用にかかる料金は非常に安く、費用面でも問題になりませんでした。

この方針で、テーブル作成とパーティション作成を行ってくれるバッチを Ruby で実装しました。実装するにあたって一番複雑であろう、ログをパースするための正規表現が先述のとおり AWS のドキュメントに掲載されているため、後は AWS SDK を使って実装していくのみでした。このバッチは、社内のバッチ実行基盤である kuroko2 を使って日次で実行されるように設定しました。

このように作成したバッチは私の就業型インターンの期間中に運用が始まり、現在に至るまで特に大きな問題もなく動き続けています。

まとめ

この記事では、ALB のアクセスログを Athena でクエリしやすくするためにバッチを書いた話をご紹介しました。このシステムによって、日々の業務の中でほんの時々必要となるちょっとした作業を減らすことができました。同時に、Athena や Glue にそこまで詳しくなくても SQL がある程度書ければアクセスログをクエリできるという状態を作ることもできました。

このように、インターンの中で現実の問題を解決でき、社内のエンジニアリング環境を少し向上できた、面白いインターンであったと今更ながら考えています。

最後に、クックパッドでは、サービス開発や基盤開発にチャレンジする就業型インターンを通年募集しています。気になった方は是非ウェブサイトよりご応募ください:

info.cookpad.com

*1:すぐ下に書いてあるように、更にパーティションについても絞り込む必要はあります。

*2:更に、ALB ログは 1 行 1 アクセスログで保存されているテキストファイルなので、Parquet などのデータフォーマットに変換することでよりスキャンサイズを落とせる可能性があります。ただし今回は変換にかかる金銭コストと ALB ログへのクエリ頻度を天秤にかけ、この変換までは行っていません。

*3:もっと言うと不要になったテーブルやパーティションを削除しても良いです。

/* */ @import "/css/theme/report/report.css"; /* */ /* */ body{ background-image: url('https://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20140527/20140527163350.png'); background-repeat: repeat-x; background-color:transparent; background-attachment: scroll; background-position: left top;} /* */ body{ border-top: 3px solid orange; color: #3c3c3c; font-family: 'Helvetica Neue', Helvetica, 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo, Osaka, 'MS Pゴシック', sans-serif; line-height: 1.8; font-size: 16px; } a { text-decoration: underline; color: #693e1c; } a:hover { color: #80400e; text-decoration: underline; } .entry-title a{ color: rgb(176, 108, 28); cursor: auto; display: inline; font-family: 'Helvetica Neue', Helvetica, 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo, Osaka, 'MS Pゴシック', sans-serif; font-size: 30px; font-weight: bold; height: auto; line-height: 40.5px; text-decoration: underline solid rgb(176, 108, 28); width: auto; line-height: 1.35; } .date a { color: #9b8b6c; font-size: 14px; text-decoration: none; font-weight: normal; } .urllist-title-link { font-size: 14px; } /* Recent Entries */ .recent-entries a{ color: #693e1c; } .recent-entries a:visited { color: #4d2200; text-decoration: none; } .hatena-module-recent-entries li { padding-bottom: 8px; border-bottom-width: 0px; } /*Widget*/ .hatena-module-body li { list-style-type: circle; } .hatena-module-body a{ text-decoration: none; } .hatena-module-body a:hover{ text-decoration: underline; } /* Widget name */ .hatena-module-title, .hatena-module-title a{ color: #b06c1c; margin-top: 20px; margin-bottom: 7px; } /* work frame*/ #container { width: 970px; text-align: center; margin: 0 auto; background: transparent; padding: 0 30px; } #wrapper { float: left; overflow: hidden; width: 660px; } #box2 { width: 240px; float: right; font-size: 14px; word-wrap: break-word; } /*#blog-title-inner{*/ /*margin-top: 3px;*/ /*height: 125px;*/ /*background-position: left 0px;*/ /*}*/ /*.header-image-only #blog-title-inner {*/ /*background-repeat: no-repeat;*/ /*position: relative;*/ /*height: 200px;*/ /*display: none;*/ /*}*/ /*#blog-title {*/ /*margin-top: 3px;*/ /*height: 125px;*/ /*background-image: url('https://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20140527/20140527172848.png');*/ /*background-repeat: no-repeat;*/ /*background-position: left 0px;*/ /*}*/