AWSでWEBサイトをホストするときのログのライフサイクルについて、まとめてみました。
WEBサーバーの一般的なログの扱いは以下のような感じだと思います。
- 各インスタンスのアクセスログを1箇所に集める
- 複数のログファイルを1ファイルにまとめてソートする
- 集計をする
- 古いログのバックアップをとり、削除する
これをAWSで行なってみると例えば以下のようになります。
- fluentdを使って各インスタンスのログを1つのバケットAに送る
- EMRで1つにまとめてソート。別のバケットBに保存
- EMRで集計もして、別のバケットBに保存
- バケットAの期限が過ぎたものをGlacierに送る
図にすると以下のようなイメージです。
それでは1つずつやってみます。
1.各インスタンスのアクセスログを1箇所に集める
これはfluentdでtail→s3で行います。
WEBサーバーのインスタンスで、以下の用にfluentdをインストールします。
# cat /etc/yum.repos.d/td.repo [treasuredata] name=TreasureData baseurl=http://packages.treasure-data.com/redhat/$basearch gpgcheck=0 # yum install td-agent -y
次にtd-agent.confを設定しますが、ログファイル名があとで重複しないように、
td-agent.conf.tmplを使用します。以下のようにtime_slice_formatに${hostname}というプレイスホルダを入れます。
また、yyyymmddのフォーマットでフォルダを切って、そこに保存するようにします。
# cat /etc/td-agent/td-agent.conf.tmpl <source> type tail format apache path /var/log/httpd/access_log tag apache.access </source> <match apache.access> type s3 aws_key_id xxxxxxxxxxxxxxxxxxxx aws_sec_key yyyyyyyyyyyyyyyyyyyyyyyyyyy s3_bucket memorycraft-log path logs/ buffer_path /var/log/fluent/s3 time_slice_format %Y%m%d/${hostname}-%H time_slice_wait 10m </match>
次に、/etc/init.d/td-agent の最初の方に、以下のようにtmplをホスト名で書き換える処理を入れます。
#!/bin/bash # # /etc/rc.d/init.d/td-agent # # chkconfig: - 80 20 # description: td-agent # processname: td-agent # pidfile: /var/run/td-agent/td-agent.pid # ### BEGIN INIT INFO # Provides: td-agent # Default-Stop: 0 1 6 # Required-Start: $local_fs # Required-Stop: $local_fs # Short-Description: td-agent's init script # Description: td-agent is a data collector ### END INIT INFO # Source function library. . /etc/init.d/functions sed -e "s/\${hostname}/`hostname`/g" /etc/td-agent/td-agent.conf.tmpl > /etc/td-agent/td-agent.conf …
そして、td-agentとhttpdを起動時ONにします。
# chkconfig httpd on # chkconfig td-agent on
ここまでやったら、/var/www/html に適当なヘルスチェックファイルを置いて、AMIにかためます。
その後、何台か起動し、ELBにぶら下げます。
いくつかのURLでアクセスをして、ログの実績を作っておきます。
しばらくすると、S3のmemorycraft-logバケットに各ホストのログが溜まってきます。
データは以下のようなフォーマットになります。
日付<タブ文字>fluentタグ<タブ文字>access_logをJSON文字列化したもの
2012-12-21T04:22:02+09:00 apache.access {"host":"10.158.169.28","user":"-","method":"GET","path":"/favicon.ico","code":"404","size":"321","referer":"-","agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.101 Safari/537.11"}
2.複数のログファイルを1ファイルにまとめてソートする
EMRをつかいます。
あとで集計も行うので、ここでは手軽にSQLライクな集計ができるHiveを利用してみます。
Hiveスクリプトは以下の流れです。
入力データを指定
アクセスログの溜まったmemorycraft-logバケットをテーブルとしてアクセスできるようにします。
実行時に日付指定をするために変数DATEを渡すので、それを使ってfluentdが保存するyyyymmddのフォルダを指定します。
CREATE EXTERNAL TABLE IF NOT EXISTS fluentLog (dt string, tag string, json string) ROW FORMAT DELIMITED FIELDS TERMINATED BY '\t' LINES TERMINATED BY '\n' LOCATION 's3://memorycraft-log/logs/${DATE}';
出力データを設定
1ファイルにまとめてソートしたデータの出力先をmemorycraft-log-archiveバケットのarchives/yyyymmddというフォルダで出力するように設定します。このときカラムはfluentdが送る日付部分(dt)と、データのJSON部分の内容(host~agent)にします。また、fluentdのS3出力フォーマットに合わせて、フィールドと改行をそれぞれタブ区切り、改行コードで指定します。
CREATE EXTERNAL TABLE IF NOT EXISTS archiveLog (dt string, host string, user string, method string, path string, code string, size bigint, referer string, agent string) ROW FORMAT DELIMITED FIELDS TERMINATED BY '\t' LINES TERMINATED BY '\n' LOCATION 's3://memorycraft-log-archive/archives/${DATE}';
まとめてソート
INSERT ~ SELECT ~
を利用すると、入力ソースから整形して出力先へ出力することができます。
整形するときは LATERAL VIEW json_tuple関数で、JSONをパース分割すると通常のカラムとしてSELECTできます。
また、fluentの日付部分(dt)はJSON外なのでそのままカラムとして扱えます。
そして、日付部分dtが今回の対象日時に限定するように念のため絞っておきます。
アクセスログのフォーマットはyyyy-mm-ddなので、yyyymmddの形にしてから比較します。
${DATE}をyyyy-mm-ddに整形したほうがパフォーマンスはいいと思います。またフォルダ名など全体的にフォーマットをyyyy-mm-ddに統一すればこのような変換は必要ありません。今回は気にせず、yyyymmddで進めます。
INSERT OVERWRITE TABLE archiveLog SELECT dt,host,user,method,path,code,size,referer,agent FROM fluentLog LATERAL VIEW json_tuple(fluentLog.json, 'host', 'user', 'method', 'path', 'code', 'size', 'referer', 'agent') j AS host,user,method, path, code, size, referer, agent WHERE regexp_replace(substr(dt, 0, 10), '-', '') = '${DATE}' ORDER BY dt;
このクエリを実行すると's3://memorycraft-log-archive/archives/yyyymmdd/に日付でソートされたデータが1つのファイルにまとまって出力されることになります。
3.集計をする
出力データを設定
もう一つ、それぞれのURLに何回アクセスがあったかを集計したデータの出力先をmemorycraft-log-archiveバケットのstats/yyyymmddというフォルダに設定します。
CREATE EXTERNAL TABLE IF NOT EXISTS statLog (path string, cnt bigint) ROW FORMAT DELIMITED FIELDS TERMINATED BY '\t' LINES TERMINATED BY '\n' LOCATION 's3://memorycraft-log-archive/stats/${DATE}';
集計
ここではpathでGROUP BYすることで、pathごとのアクセス回数をSELECTすることができます。
INSERT OVERWRITE TABLE statLog SELECT path,COUNT(dt) FROM fluentLog LATERAL VIEW json_tuple(fluentLog.json, 'host', 'user', 'method', 'path', 'code', 'size', 'referer', 'agent') j AS host,user,method, path, code, size, referer, agent WHERE regexp_replace(substr(dt, 0, 10), '-', '') = '${DATE}' GROUP BY path ORDER BY path;
このクエリを実行すると's3://memorycraft-log-archive/stats/yyyymmdd/にpathとそのpathへのアクセス回数のデータが1つのファイルにまとまって出力されることになります。
ここまでのクエリをすべて1つのファイルにまとめて、whatislog.qという名前で、s3://memorycraft-hive/にアップロードします。hiveはこのスクリプトをつかってジョブを実行します。
whatislog.q
CREATE EXTERNAL TABLE IF NOT EXISTS fluentLog (dt string, tag string, json string) ROW FORMAT DELIMITED FIELDS TERMINATED BY '\t' LINES TERMINATED BY '\n' LOCATION 's3://memorycraft-log/logs/${DATE}'; CREATE EXTERNAL TABLE IF NOT EXISTS archiveLog (dt string, host string, user string, method string, path string, code string, size bigint, referer string, agent string) ROW FORMAT DELIMITED FIELDS TERMINATED BY '\t' LINES TERMINATED BY '\n' LOCATION 's3://memorycraft-log-archive/archives/${DATE}'; INSERT OVERWRITE TABLE archiveLog SELECT dt,host,user,method,path,code,size,referer,agent FROM fluentLog LATERAL VIEW json_tuple(fluentLog.json, 'host', 'user', 'method', 'path', 'code', 'size', 'referer', 'agent') j AS host,user,method, path, code, size, referer, agent WHERE regexp_replace(substr(dt, 0, 10), '-', '') = '${DATE}' ORDER BY dt; CREATE EXTERNAL TABLE IF NOT EXISTS statLog (path string, cnt bigint) ROW FORMAT DELIMITED FIELDS TERMINATED BY '\t' LINES TERMINATED BY '\n' LOCATION 's3://memorycraft-log-archive/stats/${DATE}'; INSERT OVERWRITE TABLE statLog SELECT path,COUNT(dt) FROM fluentLog LATERAL VIEW json_tuple(fluentLog.json, 'host', 'user', 'method', 'path', 'code', 'size', 'referer', 'agent') j AS host,user,method, path, code, size, referer, agent WHERE regexp_replace(substr(dt, 0, 10), '-', '') = '${DATE}' GROUP BY path ORDER BY path;
次に、ローカルの操作端末にelastic map reduceのコマンドラインインターフェースをインストールします。
# wget http://elasticmapreduce.s3.amazonaws.com/elastic-mapreduce-ruby.zip # unzip elastic-mapreduce-ruby.zip # yum install ruby
設定情報をcredentials.jsonに記載して保存します。
# cat credential.json { "access_id":"xxxxxxxxxxxxxxxxxx", "private_key":"yyyyyyyyyyyyyyyyyyyyyyyyyyyyyy", "keypair":"memorycraft", "key-pair-file":"./memorycraft.pem", "log_uri":"s3://memorycraft-hive-log/", "region":"ap-northeast-1" }
そして、以下のようにhiveにDATE引数を渡してコマンドを実行し、ジョブを開始します。
# SCRIPT=s3://memorycraft-hive # ./elastic-mapreduce --create --name "What is Log" --num-instances 2 --master-instance-type m1.small --slave-instance-type m1.small --hive-script --arg $SCRIPT/whatislog.q --args -d,DATE='20121221' Created job flow j-1WQTYOTXBCNXC
するとAWSコンソールのElasticMapReduceにジョブフローが追加され、Debugダイアログを見ると実行中である旨の表示を確認できます。
処理が終わるとフローが完了表示になります。
S3の出力先ディレクトリをみると、ファイルが出力されています。
まとめてソートの出力ファイルをダウンロードしてみると、ちゃんとまとめてソートされていることがわかります。
2012-12-21T04:18:38+09:00 10.158.169.28 - GET /index.html 200 5 - ELB-HealthChecker/1.0 2012-12-21T04:19:08+09:00 10.158.169.28 - GET /index.html 200 5 - ELB-HealthChecker/1.0 2012-12-21T04:19:38+09:00 10.158.169.28 - GET /index.html 200 5 - ELB-HealthChecker/1.0 2012-12-21T04:19:38+09:00 10.158.169.28 - GET /index.html 200 5 - ELB-HealthChecker/1.0 2012-12-21T04:20:08+09:00 10.158.169.28 - GET /index.html 200 5 - ELB-HealthChecker/1.0 2012-12-21T04:20:08+09:00 10.158.169.28 - GET /index.html 200 5 - ELB-HealthChecker/1.0 2012-12-21T04:20:22+09:00 10.158.169.28 - GET / 304 \N - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.101 Safari/537.11 2012-12-21T04:20:22+09:00 10.158.169.28 - GET /favicon.ico 404 321 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.101 Safari/537.11 2012-12-21T04:20:25+09:00 10.158.169.28 - GET /favicon.ico 404 321 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.101 Safari/537.11 2012-12-21T04:20:25+09:00 10.158.169.28 - GET /assets/img/log.png 404 328 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.101 Safari/537.11 2012-12-21T04:20:38+09:00 10.158.169.28 - GET /index.html 200 5 - ELB-HealthChecker/1.0 2012-12-21T04:20:38+09:00 10.158.169.28 - GET /favicon.ico 404 321 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.101 Safari/537.11 2012-12-21T04:20:38+09:00 10.158.169.28 - GET /assets/img/title.png 404 330 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.101 Safari/537.11 2012-12-21T04:20:38+09:00 10.158.169.28 - GET /index.html 200 5 - ELB-HealthChecker/1.0 2012-12-21T04:20:41+09:00 10.158.169.28 - GET /assets/img/title1.png 404 331 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.101 Safari/537.11 2012-12-21T04:20:41+09:00 10.158.169.28 - GET /favicon.ico 404 321 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.101 Safari/537.11 2012-12-21T04:20:43+09:00 10.158.169.28 - GET /favicon.ico 404 321 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.101 Safari/537.11 2012-12-21T04:20:43+09:00 10.158.169.28 - GET /assets/img/title2.png 404 331 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.101 Safari/537.11 2012-12-21T04:20:49+09:00 10.158.169.28 - GET /assets/img/loading.gif 404 332 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.101 Safari/537.11 2012-12-21T04:20:49+09:00 10.158.169.28 - GET /favicon.ico 404 321 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.101 Safari/537.11 2012-12-21T04:20:53+09:00 10.158.169.28 - GET /favicon.ico 404 321 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.101 Safari/537.11 2012-12-21T04:20:53+09:00 10.158.169.28 - GET /assets/js/client.js 404 329 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.101 Safari/537.11 2012-12-21T04:20:57+09:00 10.158.169.28 - GET /assets/js/jquery.min.js 404 333 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.101 Safari/537.11 2012-12-21T04:20:57+09:00 10.158.169.28 - GET /favicon.ico 404 321 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.101 Safari/537.11 2012-12-21T04:21:01+09:00 10.158.169.28 - GET /favicon.ico 404 321 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.101 Safari/537.11 2012-12-21T04:21:01+09:00 10.158.169.28 - GET /assets/css/bootstrap.min.css 404 338 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.101 Safari/537.11 2012-12-21T04:21:03+09:00 10.158.169.28 - GET /favicon.ico 404 321 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.101 Safari/537.11 2012-12-21T04:21:08+09:00 10.158.169.28 - GET /index.html 200 5 - ELB-HealthChecker/1.0 2012-12-21T04:21:08+09:00 10.158.169.28 - GET /index.html 200 5 - ELB-HealthChecker/1.0 2012-12-21T04:21:09+09:00 10.158.169.28 - GET /favicon.ico 404 321 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.101 Safari/537.11 2012-12-21T04:21:12+09:00 10.158.169.28 - GET /favicon.ico 404 321 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.101 Safari/537.11 2012-12-21T04:21:12+09:00 10.158.169.28 - GET /test 404 314 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.101 Safari/537.11 2012-12-21T04:21:15+09:00 10.158.169.28 - GET /favicon.ico 404 321 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.101 Safari/537.11 2012-12-21T04:21:15+09:00 10.158.169.28 - GET /singin 404 316 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.101 Safari/537.11 2012-12-21T04:21:22+09:00 10.158.169.28 - GET /signup 404 316 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.101 Safari/537.11 2012-12-21T04:21:22+09:00 10.158.169.28 - GET /favicon.ico 404 321 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.101 Safari/537.11 2012-12-21T04:21:26+09:00 10.158.169.28 - GET /favicon.ico 404 321 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.101 Safari/537.11 2012-12-21T04:21:37+09:00 10.158.169.28 - GET /favicon.ico 404 321 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.101 Safari/537.11 2012-12-21T04:21:37+09:00 10.158.169.28 - GET /download/test.zip 404 327 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.101 Safari/537.11 2012-12-21T04:21:38+09:00 10.158.169.28 - GET /index.html 200 5 - ELB-HealthChecker/1.0 2012-12-21T04:21:38+09:00 10.158.169.28 - GET /index.html 200 5 - ELB-HealthChecker/1.0 2012-12-21T04:21:40+09:00 10.158.169.28 - GET /favicon.ico 404 321 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.101 Safari/537.11 2012-12-21T04:21:58+09:00 10.158.169.28 - GET /assets/img/logo.png 404 329 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.101 Safari/537.11 2012-12-21T04:21:58+09:00 10.158.169.28 - GET /favicon.ico 404 321 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.101 Safari/537.11 2012-12-21T04:22:02+09:00 10.158.169.28 - GET /favicon.ico 404 321 - Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.101 Safari/537.11 2012-12-21T04:22:08+09:00 10.158.169.28 - GET /index.html 200 5 - ELB-HealthChecker/1.0 2012-12-21T04:22:08+09:00 10.158.169.28 - GET /index.html 200 5 - ELB-HealthChecker/1.0
また、集計の出力ファイルをダウンロードしてみると、以下のようにURLと回数が表示されていることがわかります。
/ 1 /assets/css/bootstrap.min.css 1 /assets/img/loading.gif 1 /assets/img/log.png 1 /assets/img/logo.png 1 /assets/img/title.png 1 /assets/img/title1.png 1 /assets/img/title2.png 1 /assets/js/client.js 1 /assets/js/jquery.min.js 1 /download/test.zip 1 /favicon.ico 19 /index.html 14 /signup 1 /singin 1 /test 1
マージ、集計ともうまくいったようです。
また、Hiveはスクリプトをアップするのではなく、EMRのマスタノードからHiveコンソールに入ってクエリを実行することもできるので、お客さんからイレギュラーな集計処理を頼まれた時も手軽に集計ができます。
4.古いログのバックアップをとり、削除する
次はfluentから送られてきて溜まったログをS3からGlaceirに送る設定をします。
Glaceirは入出力にかなり時間がかかるものの、S3よりも更に安価に大容量のデータを保存することができます。
削除したくないけど、使用はしばらくしないようなログの保存先に向いています。
今回の場合はmemorycraft-logのデータで、集計が終わって期限が1週間をすぎたものをGlaceirにアーカイブするように設定してみます。
AWSコンソールのS3バケットプロパティで、LifeCycleセクションから「Add Rule」ボタンを押します。
ダイアログが表示されるので、ルールに名前をつけて、「Apply to Entire Bucket」にチェックを入れます。
もしこのバケットが他のデータも含んでいたり、アーカイブに条件をつけたい場合は、チェックを入れずに対象としたいファイル名のプリフィクスを入力します。
下部の「Add Transition」をクリックし、アーカイブされるまでの日数を入力します。ここでは7を入れます。
ファイルの作成日時から7日たつとアーカイブされる設定です。
もし、日付を指定したいのであれば、「Time Period Format」をDateのほうにチェックを入れて日付を入力します。
OKを押して設定完了です。
これで、不要なファイルはS3からGlaceirに送られるようになります。
といっても、このブログを書きながらやったので、まだGlacierに送られないので結果は追って観察してみたいと思います。
こんな感じで、AWS内でログのライフサイクルをひと通り眺めてみました。
もちろんS3やCloudfrontのログにも応用できますし、EC2内のログのローテーションや自動削除、EMRによる更に複雑な集計など、いろいろなバリエーションが考えられます。
EC2インスタンスの外側でこれだけのことができるのであればとても気が楽ですね。便利だー。