アクトインディ開発者ブログ

子供とお出かけ情報「いこーよ」を運営する、アクトインディ株式会社の開発者ブログです

S3のフォルダーが日毎にちゃんと作られているかLambdaで監視する

こんにちは!!こんにちは!!
インフラエンジニアのyamamotoです。

最近、しいたけ栽培セット にハマっています。モリモリしいたけが生えてくるのでとっても面白いです!
生のしいたけというのも普段なかなかお目にかかれないので、秋の味覚を楽しめてイイですよ!

しいたk…ログがちゃんと出力されているか?

当社のあるプロダクトではAWSを使用していますが、ログをAthenaで読むために、S3に日毎のフォルダーに分けて出力するようにしています。
ログの出力のしかたについては過去の記事をご参照ください。 tech.actindi.net

ログって使わないときはつい放置しがちになりますよねー。いつの間にか出力が止まっていたりしても気づかなかったり。
そこで、ログが毎日ちゃんと出力されているか監視するために、スクリプトを書いてLambdaで監視するようにしてみました。

コードの説明

ざっくり、コードはこんな感じになりました。

const {
  S3_BUCKET_NAME,
  S3_CHECK_PATTERN1,
  S3_CHECK_PATTERN2,
  S3_CHECK_PATTERN3,
  SNS_TOPICARN,
  SNS_REGION,
  SNS_MESSAGE_TITLE,
  SLACK_URL,
  SLACK_CHANNEL,
} = process.env;

const AWS = require('aws-sdk');

const s3 = new AWS.S3();
const sns = new AWS.SNS({
  apiVersion: '2010-03-31',
  region: SNS_REGION,
});

const checkPattern = [S3_CHECK_PATTERN1, S3_CHECK_PATTERN2, S3_CHECK_PATTERN3];

function formatDate(date, format) {
  let ret = format;
  if (!ret) ret = 'YYYY-MM-DD';
  ret = ret.replace(/YYYY/g, date.getFullYear());
  ret = ret.replace(/MM/g, `0${(date.getMonth() + 1)}`.slice(-2));
  ret = ret.replace(/DD/g, `0${date.getDate()}`.slice(-2));
  return ret;
}

async function checkS3Object(bucket, pat) {
  const params = {
    Prefix: formatDate(new Date(), pat),
    Bucket: bucket,
    MaxKeys: 1,
    StartAfter: formatDate(new Date(), pat),
  };

  console.info(`Checking S3 log directory ${bucket}/${params.Prefix}`);
  const res = await s3.listObjectsV2(params).promise();
  if (res.KeyCount === 0) {
    console.info('Data listobj: ', res);
    return 1;
  }
  return 0;
}

async function checkS3Status(bucket, pattern) {
  let ret = 0;
  let res = 0;
  for (let i = 0; i < pattern.length; i += 1) {
    if (pattern[i]) {
      res = await checkS3Object(bucket, pattern[i]);
      if (res === 1) {
        ret = `${bucket}/${pattern[i]}`;
        break;
      }
    }
  }
  return ret;
}

async function publishSNS(arn, title, message) {
  const params = {
    Message: message,
    Subject: title,
    TopicArn: arn,
  };

  try {
    await sns.publish(params).promise();
  } catch (err) {
    console.error('Error publishing: ', err);
  }
}

async function publishSlack(slackurl, slackchannel, message) {
  const { IncomingWebhook } = require('@slack/webhook');
  const slackwebhook = new IncomingWebhook(slackurl, {
    channel: slackchannel,
  });

  try {
    await slackwebhook.send(message);
  } catch (err) {
    console.error('Error Slack: ', err);
  }
}

exports.handler = async () => {
  const ret = await checkS3Status(S3_BUCKET_NAME, checkPattern);
  if (ret !== 0) {
    const msg = `Error! S3 log directory ${ret} is not known!`;
    await publishSNS(SNS_TOPICARN, SNS_MESSAGE_TITLE, msg);
    if (SLACK_URL !== '') {
      await publishSlack(SLACK_URL, SLACK_CHANNEL, msg);
    }
    console.error(msg);
  }
};

フォルダー名の形式を受け取って、それにマッチしたものが無いとSNS→メールと、Slackで通知するようになっています。
当社の例ではフォルダー名は「/year={年}/month={月}/day={日}/ 」なので、日付をフォーマットに当て込む関数を簡易的に使っています。
また、AWSのオブジェクトは非同期で扱うのでasync・awaitを使って受け渡しています。

意外と迷ったのが、フォルダーの存在有無をどう確認するか、というところです。
S3のフォルダーの有無を確認するには、s3.listObjectV2() メソッドを使用しています。

あと、SlackのWebhookを使った通知については、下記を参照しました。
https://slack.dev/node-slack-sdk/webhook

Lambdaに仕込む

これを、Lambdaに仕込みます。
トリガーとして CloudWatch Event を使い、毎日特定の時間に実行するようにします。日毎の場合、日付が変わってある程度時間が過ぎてからにしています。
リソースについては、監視するS3オブジェクトに対する s3:ListBucket 権限と、SNSの sns:Publish 権限が追加で必要になります。
あと、コードの冒頭にある環境変数を設定する必要があります。

さいごに

アクトインディでは、しいt…Webエンジニアを募集しています! actindi.net