By

AWS Lambda と NewRelic Insightsを使って S3の転送量を監視する

はじめに

デジカでは、Amazon AWS のサービスをいろいろ使っていますが、コスト面から見ると、S3からの転送料金が半分近くを占めています。

そして、そのトラフィックの半分以上がダウンロード販売しているパッケージソフトのダウンロードです。これには、試用版やランタイムも含まれており、新製品の販売やセールなどに伴って、使用量や形態も大きく変動します。

そこで、この S3 の転送量を監視して、場合によっては変動の要因を見つけることができるようにしなければなりません。

その仕組みを下記のツールの組み合わせで作成した所、適材適所でいい感じでできたので、紹介したいと思います。

  • S3 から提供されるアクセスログ
  • AWS Lambda によって、NewRelic のカスタムイベントとして記録
  • NRQL による、ログの分析
  • Sensu による、使用量や変動の監視

New Relic Insights は、ダッシュボードツールなので、API を叩いて任意のデータを送れば、好きなデータをいい感じのグラフで見えるようになります。デジカでは、New Relic 製品をいろいろ活用しているので、今回はデータ管理ツールとして、New Relic Insights を使います。

Insights には、データを蓄積するストレージの機能と、蓄積したデータに対するクエリーを登録してダッシュボードを作る機能があります。今回は、次のようなダッシュボードを作りました(詳細は後述)。

一番重要な、ログを NewRelic に転送する AWS Lambda Function は、下記で公開していますので、NewRelic の有料アカウントを取れば、同じように分析することができます。

S3 から提供されるアクセスログ

S3 のバケットのプロパティに、「ログ記録」という項目があります。ここはデフォルトではオフになっていますが、ここにログ出力先のバケットを設定することで、アクセスログを取ることができます。

データ用のバケットと同じバケットを指定することもできますが、後述の Lambda Function を使用する場合は、別の、ログ専用のバケットを指定してください。

とられるログは次のような形式です。

79a59df900b949e55d96a1e698fbacedfd6e09d98eacf8f8d5218e7cd47ef2be mybucket [06/Feb/2014:00:00:38 +0000] 192.0.2.3 79a59df900b949e55d96a1e698fbacedfd6e09d98eacf8f8d5218e7cd47ef2be 3E57427F3EXAMPLE REST.GET.VERSIONING - "GET /mybucket?versioning HTTP/1.1" 200 - 113 - 7 - "-" "S3Console/0.4" -
79a59df900b949e55d96a1e698fbacedfd6e09d98eacf8f8d5218e7cd47ef2be mybucket [06/Feb/2014:00:00:38 +0000] 192.0.2.3 79a59df900b949e55d96a1e698fbacedfd6e09d98eacf8f8d5218e7cd47ef2be 891CE47D2EXAMPLE REST.GET.LOGGING_STATUS - "GET /mybucket?logging HTTP/1.1" 200 - 242 - 11 - "-" "S3Console/0.4" -
79a59df900b949e55d96a1e698fbacedfd6e09d98eacf8f8d5218e7cd47ef2be mybucket [06/Feb/2014:00:00:38 +0000] 192.0.2.3 79a59df900b949e55d96a1e698fbacedfd6e09d98eacf8f8d5218e7cd47ef2be A1206F460EXAMPLE REST.GET.BUCKETPOLICY - "GET /mybucket?policy HTTP/1.1" 404 NoSuchBucketPolicy 297 - 38 - "-" "S3Console/0.4" -

このファイルのフィールドについては、下記のドキュメントに記述されています。

AWS Lambda によって、NewRelic のカスタムイベントとして記録

このファイルを NewRelic Insights のカスタムイベントとして登録する Lambda Function を作成しました。

使い方

lambda-config.sample.jslambda-config.js にリネームして、下記の項目を設定してください。

  • region: データ用バケットのあるリージョン
  • role: このファンクション用の IAM ROLE の arn
  • functionName: 必要があれば修正

設定する IAM ROLE には AmazonS3ReadOnlyAccessCloudWatchLogsFullAccess を設定してください。

src/config.sample.jssrc/config.js にリネームして、下記の項目を設定してください。

  • exports.insertKey: NewRelic の Insert API Key
  • exports.accountId: NewRelic の Account ID

これらの項目については、下記のドキュメントに取得方法が説明されています。

以上を設定したら、下記のコマンドでアップロードできます。

$ npm install
$ gulp deploy

最後に、トリガーを設定します。AWS Web Console の Lambda を開くと、アップロードしたファンクションが表示されるので、”Event Source” として下記の項目を設定してください。

  • Event Source Type: S3
  • Bucket: ログ用バケットのバケット名
  • Event Type: Object Created (ALL)
  • Prefix/Suffix: 任意(空白で良い)

NRQL による、ログの分析

上記の設定を行って、しばらくすると、Insights にログのデータが送られます。 ”Data Explorer” で “Custom Events” の “S3Logs” を指定して、データが送信されていることを確認してください。

NewRelic Insights では、NRQL という SQLライクな言語によるクエリーでデータを分析することができます。

私が使っているクエリーは次のようなものです。

1日分の30分ごとの使用量分析(バケット別)
SELECT sum(sent) FROM S3Logs SINCE 1 days ago until 1 hour ago TIMESERIES 30 minutes FACET bucket

FACET というキーワードは SQL の group by と似た意味で、指定した項目ごとの計を表示します。

直近1時間のリモートアドレスごとの使用量計
SELECT sum(sent) FROM S3Logs FACET remoteAddr since 2 hours ago until 1 hour ago

大量のファイルをダウンロードしている人がいないかチェックするために使っています。単純に直近1時間なら、 since 1 hour ago で良いのですが、S3 のログの送信には平均1時間ほどの遅れがあるので、「2時間前から1時間前まで」という指定にしています。

トラフィックを使用しているファイルを探す
SELECT sum(sent) , count(sent), average(sent) from S3Logs FACET path since 1 day ago

特定バケットの使用状況をリファラーごとに表示
SELECT sum(sent) , count(sent), average(sent) from S3Logs where bucket != 'degica-xxxxxxx' FACET referrer SINCE 1 day ago

これらは毎日一回見ていますが、いつもと違うトラフィックがあった時には、 FACET WHERE SINCE の内容を変えて、さらにアドホックな分析を行います。

総コストの概算表示

SELECT filter(sum(sent)/(1024*1024*1024) , WHERE bucket = 'degica-xxxxxx') as 'us-east traffic', filter(sum(sent)/(1024*1024*1024) , WHERE bucket != 'degica-xxxxxx') as 'tokyo traffic', filter(sum(sent)*0.09/(1024*1024*1024) , WHERE bucket = 'degica-downloads') as 'us-east cost', filter(sum(sent)*0.14/(1024*1024*1024) , WHERE bucket != 'degica-xxxxxx') as 'tokyo cost' from S3Logs since 1 day ago

S3 の使用料金(単価)は、リージョンごとに違うので、バケット名で分けて、リージョン別の総使用量と料金を計算しています。(’degica-xxxxxx’ だけが us-east で他は tokyo です)

これらのNRQLのクエリーは、Insights の “Query” という所から入力します。入力中にはフィールド名やキーワードがサジェストされるので、簡単に入力することができます。

結果表示は、「グラフ」のタブがあるので、集計結果をすぐグラフ化して見ることができます。また、そのグラフをダッシュボードに登録しておくと、すぐ呼び出すことができます。

Sensu による、使用量や変動の監視

そして、Sensu のプラグインとして、このカスタムイベントに対して、クエリーを発行して、それをチェックするスクリプトを書きました。

#!/bin/bash

NRQL='SELECT sum(sent) FROM S3Logs FACET remoteAddr since 2 hours ago'
NRQL_ENCODED=`echo "$NRQL" | sed -e 's/ /%20/g' -e 's/(/%28/g' -e 's/)/%29/g' `
QUERY_KEY='[query api key for your account]'
ACCOUNT_ID='[your account]'

RESULT=`curl -s -H "Accept: application/json" -H "X-Query-Key: ${QUERY_KEY}" "https://insights-api.newrelic.com/v1/accounts/${ACCOUNT_ID}/query?nrql=$NRQL_ENCODED"`

TOTAL=`echo $RESULT | jq '.totalResult.results[].sum'`
echo "TOTAL=${TOTAL}"
if [ $TOTAL -ge 53687091200 ]
then
  echo 'Total traffic is over 50G'
  exit 1
fi
if [ $TOTAL -ge 107374182400 ]
then
  echo 'Total traffic is over 100G'
  exit 2
fi

BYIP=`echo $RESULT | jq '.facets[0].results[].sum'`
echo "BYIP=${BYIP}"
if [ $BYIP -ge 3221225472 ]
then
  echo 'BYIP traffic is over 3G'
  REMOTE_ADDR=`echo $RESULT | jq -r '.facets[0].name'`
  NRQL="SELECT * from S3Logs where remoteAddr = '${REMOTE_ADDR}' SINCE 3 hours ago"
  NRQL_ENCODED=`echo "$NRQL" | sed 's/ /%20/g' | sed 's/(/%28/g' | sed 's/)/%29/g' `
  RESULT=`curl -s -H "Accept: application/json" -H "X-Query-Key: ${QUERY_KEY}" "https://insights-api.newrelic.com/v1/accounts/${ACCOUNT_ID}/query?nrql=$NRQL_ENCODED"`
  echo "Here's last access of ${REMOTE_ADDR}"
  echo $RESULT | jq '.results[].events[0]'
  echo "Run '${NRQL}' on Insights for detail"
  exit 1
fi

ポイントは、curl で REST API のエンドポイントに、NRQLのクエリーを渡す所です。

curl -s -H "Accept: application/json" -H "X-Query-Key: ${QUERY_KEY}" "https://insights-api.newrelic.com/v1/accounts/${ACCOUNT_ID}/query?nrql=$NRQL_ENCODED"`

これで、JSON形式で集計結果が返ってくるので、あとは、jqコマンドで、必要なデータを取り出しています。

チェックしているのは、全体のトラフィックと、IPアドレス別トラフィックの最大値です。これが、ある値を超えたら、Sensuからアラートが発行されます。

アラートが発行された時は、Insights のダッシュボードから、ログを調べて対策をしています。

おまけ(サーバレス監視の可能性)

AWS Lambda の一番いいところは、一度動き出せば、ほぼそのまま放置しておけることです。

最初は、cron job でEC2のサーバにS3のログをダウンロードして、それを集計していたのですが、その方法では、そのログをダウンロードするcronが動いているか監視しなくてはいけません。また、DISKの容量とか、セキュリティアップデートの適用とか、いろいろ考える必要があります。

直接的なサービスが稼働しているサーバであれば、そういうコストをかける価値はありますが、それ以外の補助的な処理をするためのサーバでは、なるべく手間を減らしたいものです。しかし、サーバが稼働している以上は、手抜きの運用をしていると、その処理の重要性とは関係なく思わぬ問題を引き起こすことがあります。

ですから、弊社では、なるべく NewRelic のような外部のサービスを活用するという方針で運用していますが、自営サーバと比較するとどうしても機能や柔軟性に欠けるという問題があります。

この事例のように、AWS Lambda を外部サービスと組み合わせることが、グッドプラクティスになるケースは多くなると思います。

そこで、この記事を書くにあたって、試しに、ログの転送だけでなく監視までLambdaで行えないか試してみました。

Lambdaには、一定時間ごとにファンクションを起動する機能があります。このイベントソースを登録して、定期的にクエリーを発行して、その結果が一定以上であれば、Slack で警告するコードを書きました。エラー処理もしてない手抜きのコードですが、次のようになります。

if (event.source === 'aws.events') {
  const { accountId, queryKey } = insightsConfig;
  const query = "SELECT sum(sent) FROM S3Logs  FACET remoteAddr SINCE 2 hours ago";
  const options = {
    uri: `https://insights-api.newrelic.com/v1/accounts/${accountId}/query?nrql=${encodeURIComponent(query)}`,
    headers: {
      "Content-Type": "application/json",
       "X-Query-Key": queryKey
    }
  };
  request.get(options, (error, response, body)=>{
    if (!error && response.statusCode == 200) {
      // console.log(`success response=${JSON.stringify(response)} body=${body}`);
      const j = JSON.parse(body);
      const sumSentMax = j.facets[0].results[0].sum;
      console.log(`sum=${sumSentMax}`);
      if (sumSentMax > 1024 * 1024 * 1024) {
        const msg = `s3 traffic is ${sumSentMax} from ${j.facets[0].name}`;
        const {slackOpts} = insightsConfig;
        const options = {
          uri: 'https://slack.com/api/chat.postMessage',
          form: {
            token: slackOpts.token,
            channel: slackOpts.channel,
            text: msg,
            username: slackOpts.username
          }
        };
        request.post(options, (error, response, body)=>{
          console.log(`posted to slack ${response}`);
        });
      };
      context.succeed('success');
    } else {
      console.log(`error ${error}`);
      console.log(`response=${JSON.stringify(response)}`);
      context.fail(`Insights api returns error ${error}`);
    }
  });
}

これだと、同じ警告メッセージがなんども表示されてしまうという問題がありますが、一応の目的を達成することはできました。

今後も、AWS Lambda を活用して、EC2 のインスタンスの処理を減らしていきたいと思っています。

一緒にユニークな決済サービスを作ってくれる Rails エンジニアを募集中です!
多国籍なメンバーと一緒に仕事をしてみませんか?詳細はこちらのリンクです:D