前書き
Amazon Loadbalancer(ELB)を使っていると、『ELBのAccess logを解析して特定の条件(例えばStatus code != 200)が存在すればAlarmを飛ばす』と言う事をやりたい事が多いと思います。
Status CodeであればCloudWatchのHTTPCode_Backend_5XX
等のMetricsを使えば簡単に出来ます(ELBのCloud Watch Metricはここ参照)。このMetricsの条件に当てはまった時に、email等のAlarmを飛ばす様にAWS SNSを設定すれば良いです。が、これでAlarm通知は受け取れるんですが、Requestの詳細(URLとかRequest元のIPとか)を確認しようと思ったときに、S3に書き出されているELBの生Access logをみるしか有りません。これ、手作業でやろうとすると結構苦痛です。何故かLog fileがタブ区切りでもカンマ区切りでも無く、半角スペース区切りですし、、、。
ELBがS3にLogを書き出したタイミングで、AWS Lambdaを起動させて、Lambdaのnode.js内でAccess logを解析して、必要な情報を切り出してEmailの本文に含めれば楽ジャン、と思い、その設定手順やLambdaのscriptをチラ裏しておきます。
設定でのハマり箇所
- ELBのLogがS3に吐きだされるAWS Regionと同じRegionに、Lambda, SNS Topicを作成してください
- LambdaのFunctionを作成すると、Lambda functionを動かす為のIAM Roleを新規で作る様に進められます(既存の物を使い廻す事も可能)。このIAM roleに対して、
AmazonSNSFullAccess
をAttach Policyするのを忘れずに。これによりLambda FunctionからSNS通知がさらに発火出来る様になります - Lambda functionを作成後、
Add event source
で、監視するS3 bucketを設定して下さい - SNSの
Topic
とSubscriptions
をLambdaと同じAWS regionに作成し、Topicを発火させるとemail通知が行くように事前設定しておいて下さい。また TopicのARNをメモしておいてください
Debugでのハマり箇所
- まず、Lambdaの編集画面で、左ペインの
Sample Event
にS3 Put
を指定して、Invoke
をクリックすると、Error getting object HappyFace.jpg from bucket sourcebucket...
というエラーがでます。"key": "HappyFace.jpg"
と"name": "sourcebucket"
を先ほどAdd event source
で設定したS3 bucket名とそのbucketに実際にあるlog fileに書き換えて、再度Invoke
をクリックすると、このlog fileがS3 bucketに追加されたという想定でScriptが動作してくれます - Lambdaの編集画面で
Invoke
させて動かない場合は、CloudWatch -> LogsにFullのDebug logが吐き出されます
Lambdaのcode
- TopicArnは自分の物に置き換えてください
- 処理の流れは以下の通りです
- S3 Bucketに変更があったら、1行づつ取り出して、
checkString
に掛ける -
checkString
はELB Access Logの仕様を元に、半角スペース区切りで9番目の値がbackend_status_code
、区切りの2番目がURL
、、、と必要な要素を取り出す - Status Codeが200以外の場合は、その行を
snsParams.Message
に追記していき、最後にsns.publish
で結果をSNS発火させる
var aws = require('aws-sdk');
var s3 = new aws.S3({apiVersion: '2006-03-01'});
var sns = new aws.SNS({apiVersion: '2010-03-31'});
var snsParams = {
Message: '',
Subject: 'ELB log check alarm',
TopicArn: 'arn:aws:sns:us-east-1:xxxxxxxx:yyyyyyyyyy'
};
exports.handler = function(event, context) {
var bucket = event.Records[0].s3.bucket.name;
var key = event.Records[0].s3.object.key;
s3.getObject({Bucket: bucket, Key: key}, function(err, data) {
if (err) {
console.log("Error getting object " + key + " from bucket " + bucket +
". Make sure they exist and your bucket is in the same region as this function.");
context.fail('Error', "Error getting file: " + err);
}
var contentBody = data.Body.toString(data.ContentEncoding);
snsParams.Message += 'Upload filename:' + key.substr(key.lastIndexOf('/') + 1) + '\n';
snsParams.Message += 'Data\tStatusCode\tSource IP\tURL\tUserAgent\n';
contentBody.split('\n').forEach(function (line) {
checkString(line);
});
sns.publish(snsParams, function() {
context.succeed("success");
});
});
};
function checkString(line) {
if (line !== '') {
var status_code = line.split(' ')[8];
var source_ip = line.split(' ')[2].split(':')[0];
if (Number(status_code) != 200) {
snsParams.Message += line.split(' ')[0].split('.')[0] + '\t' + status_code + '\t' + source_ip+ '\t'
+ line.split('"')[1] + '\t' + line.split('"')[3] + '\n' ;
}
}
}