[Node.js] AWSアカウント内にデプロイされているCloudWatch EventsトリガーのLambda関数を列挙するスクリプト

2022.04.18

広島吉川です。

定時バッチをCloudWatch Events+Lambdaで実施しているシチュエーションにおいて、プロジェクト開始から長期間経過したり、バッチの数自体も増えてくるなどの事情で徐々に全容の把握が難しくなってしまうことがあるかと思います。そこで、CloudWatch Eventsトリガーの定時実行Lambda関数一覧を取得するスクリプトがあると便利なのではないかということで書いてみました。

日頃から継続的なドキュメント化やIaC管理を行うことを第一としつつも、定期的な棚卸しとして「今実際にデプロイされているリソース」の収集を併用することの有用性はあるのではないかと思います。

環境

  • node 16.13.0
  • npm 8.1.4
  • typescript 3.9.7
  • aws-cdk 2.20.0
  • aws-cdk-lib 2.20.0
  • constructs 10.0.115
  • @aws-sdk/* 3.67.0
  • markdown-table 3.0.2

Lambda関数とEventルールを準備

まずは検証用のLambda関数とCloudWatch Eventsをデプロイします。いつも使っている東京リージョン (ap-northeast-1) だと別の検証で使ったLambda等が残っていたこともあり、今回は大阪リージョン (ap-northeast-3) にデプロイしてみました。

下記のようなAWS CDK (v2) コードを用意し npx cdk deploy を実行します。

// bin/aws-playground.ts

#!/usr/bin/env node
import 'source-map-support/register'
import * as cdk from 'aws-cdk-lib'
import { AwsPlaygroundStack } from '../lib/aws-playground-stack'

const app = new cdk.App()
new AwsPlaygroundStack(app, 'AwsSdkPlaygroundStack', {
  env: { region: 'ap-northeast-3' }, // 大阪リージョンを指定
})
// lib/aws-playground-stack.ts

import * as cdk from 'aws-cdk-lib'
import { Construct } from 'constructs'

export class AwsPlaygroundStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props)

    // Lambda関数
    for (let i = 0; i < 30; i++) {
      new cdk.aws_lambda.Function(this, `sample-fn-${i}`, {
        functionName: `sample-fn-${i}`,
        runtime: cdk.aws_lambda.Runtime.NODEJS_14_X,
        code: cdk.aws_lambda.AssetCode.fromInline(
          `export const handler = () => console.log('Hello lambda.')`
        ),
        handler: 'handler',
      })
    }

    // CloudWatch EventsトリガーのLambda関数
    for (let i = 0; i < 30; i++) {
      const lambdaFn = new cdk.aws_lambda.Function(
        this,
        `sample-batch-fn-${i}`,
        {
          functionName: `sample-batch-fn-${i}`,
          runtime: cdk.aws_lambda.Runtime.NODEJS_14_X,
          code: cdk.aws_lambda.AssetCode.fromInline(
            `export const handler = () => console.log('Hello lambda.')`
          ),
          handler: 'handler',
        }
      )
      new cdk.aws_events.Rule(this, `sample-rule-${i}`, {
        ruleName: `sample-rule-${i}`,
        schedule: cdk.aws_events.Schedule.cron({ minute: '0' }),
        targets: [new cdk.aws_events_targets.LambdaFunction(lambdaFn)],
      })
    }
  }
}
  • CloudWatch EventsトリガーでないLambda関数
  • CloudWatch Eventsトリガーを設定したLambda関数

をそれぞれ30個ずつ生成しました。

AWS SDKでLambda関数一覧を取得

続いてAWS SDK v3でリソース一覧を収集していく方法を見ていきます。

Lambda関数一覧を取得するコードは下記になります。

import { Lambda, ListFunctionsCommandOutput } from '@aws-sdk/client-lambda'

const region = 'ap-northeast-3'
const lambda = new Lambda({
  region,
})

;(async () => {
  let lambdaFnArns: string[] = []

  let nextMarker: string | null | undefined = null
  while (nextMarker !== undefined) {
    const listFunctionsResult: ListFunctionsCommandOutput =
      await lambda.listFunctions({
        Marker: nextMarker ?? undefined,
      })
    const newLambdaFnArns = listFunctionsResult.Functions!.map(
      (lambdaFn) => lambdaFn.FunctionArn!
    )
    lambdaFnArns = lambdaFnArns.concat(newLambdaFnArns)
    nextMarker = listFunctionsResult.NextMarker
  }
  console.log({ lambdaFnArns, count: lambdaFnArns.length })
})()

実行時の出力は以下です。

{
  lambdaFnArns: [
    'arn:aws:lambda:ap-northeast-3:AWS_ACCOUNT_ID:function:sample-batch-fn-6',
    'arn:aws:lambda:ap-northeast-3:AWS_ACCOUNT_ID:function:sample-batch-fn-20',
    'arn:aws:lambda:ap-northeast-3:AWS_ACCOUNT_ID:function:sample-batch-fn-5',
    '...(省略)...'
  ],
  count: 60
}

Lambda関数の件数が多い場合 (デフォルトでは50件超) はページネーションで取得する必要があり、 nextMarker がある場合は while でループを回し続ける実装としています。

AWS SDKでLambda関数に紐づくCloudWatch Eventsルールを取得

特定のLambda関数に紐づくCloudWatch Eventsルールを取得するには以下のコードになります。

import { CloudWatchEvents } from '@aws-sdk/client-cloudwatch-events'

const region = 'ap-northeast-3'
const cwEvents = new CloudWatchEvents({
  region,
})

;(async () => {
  const lambdaFnArn =
    'arn:aws:lambda:ap-northeast-3:AWS_ACCOUNT_ID:function:sample-batch-fn-0'
  const { RuleNames } = await cwEvents.listRuleNamesByTarget({
    TargetArn: lambdaFnArn,
  })
  console.log({
    lambdaFnArn,
    cwEventRuleNames: RuleNames ?? [],
  })
})()

実行時の出力は以下です。

{
  lambdaFnArn: 'arn:aws:lambda:ap-northeast-3:AWS_ACCOUNT_ID:function:sample-batch-fn-0',
  cwEventRuleNames: [ 'sample-rule-0' ]
}

当初は紐づくEventルールもLambdaの一覧取得APIから得られそうと思っていたのですが、どうも難しそうだったためCloudWatch EventsのAPIも併用しています (もしより良い方法がありましたら情報をいただければと) 。

CloudWatch EventsをトリガーとしているLambda関数一覧を取得

上記を組み合わせて、CloudWatch EventsをトリガーとしているLambda関数一覧を取得するコードが以下になります。

import { Lambda, ListFunctionsCommandOutput } from '@aws-sdk/client-lambda'
import { CloudWatchEvents } from '@aws-sdk/client-cloudwatch-events'

const region = 'ap-northeast-3'
const lambda = new Lambda({
  region,
})
const cwEvents = new CloudWatchEvents({
  region,
})

;(async () => {
  let lambdaFnArns: string[] = []

  let nextMarker: string | null | undefined = null
  while (nextMarker !== undefined) {
    const listFunctionsResult: ListFunctionsCommandOutput =
      await lambda.listFunctions({
        Marker: nextMarker ?? undefined,
      })
    const newLambdaFnArns = listFunctionsResult.Functions!.map(
      (lambdaFn) => lambdaFn.FunctionArn!
    )
    lambdaFnArns = lambdaFnArns.concat(newLambdaFnArns)
    nextMarker = listFunctionsResult.NextMarker
  }

  const lambdaFns = await Promise.all(
    lambdaFnArns.map(async (lambdaFnArn) => {
      const { RuleNames } = await cwEvents.listRuleNamesByTarget({
        TargetArn: lambdaFnArn,
      })
      return {
        lambdaFnArn,
        cwEventRuleNames: RuleNames ?? [],
      }
    })
  )
  const batchLambdaFns = lambdaFns.filter(
    (lambdaFn) => lambdaFn.cwEventRuleNames.length > 0
  )
  console.log(batchLambdaFns)
})()

出力は以下のようになります。

[
  {
    lambdaFnArn: 'arn:aws:lambda:ap-northeast-3:AWS_ACCOUNT_ID:function:sample-batch-fn-6',
    cwEventRuleNames: [ 'sample-rule-6' ]
  },
  {
    lambdaFnArn: 'arn:aws:lambda:ap-northeast-3:AWS_ACCOUNT_ID:function:sample-batch-fn-20',
    cwEventRuleNames: [ 'sample-rule-20' ]
  },
  {
    lambdaFnArn: 'arn:aws:lambda:ap-northeast-3:AWS_ACCOUNT_ID:function:sample-batch-fn-5',
    cwEventRuleNames: [ 'sample-rule-5' ]
  },
  "...(省略)..."
]

さらにMarkdownテーブルとして出力してみる

さらに取得結果をMarkdownテーブルとして出力できると見やすい形でチームに共有できそうです。これはmarkdown-tableというパッケージを使うことで比較的簡単に実現することができます。

そのコードを追加したのが以下になります。

import { Lambda, ListFunctionsCommandOutput } from '@aws-sdk/client-lambda'
import { CloudWatchEvents } from '@aws-sdk/client-cloudwatch-events'
+import { markdownTable } from 'markdown-table'
+import fs from 'fs'

const region = 'ap-northeast-3'
const lambda = new Lambda({
  region,
})
const cwEvents = new CloudWatchEvents({
  region,
})

;(async () => {
  let lambdaFnArns: string[] = []

  let nextMarker: string | null | undefined = null
  while (nextMarker !== undefined) {
    const listFunctionsResult: ListFunctionsCommandOutput =
      await lambda.listFunctions({
        Marker: nextMarker ?? undefined,
      })
    const newLambdaFnArns = listFunctionsResult.Functions!.map(
      (lambdaFn) => lambdaFn.FunctionArn!
    )
    lambdaFnArns = lambdaFnArns.concat(newLambdaFnArns)
    nextMarker = listFunctionsResult.NextMarker
  }

  const lambdaFns = await Promise.all(
    lambdaFnArns.map(async (lambdaFnArn) => {
      const { RuleNames } = await cwEvents.listRuleNamesByTarget({
        TargetArn: lambdaFnArn,
      })
      return {
        lambdaFnArn,
        cwEventRuleNames: RuleNames ?? [],
      }
    })
  )
  const batchLambdaFns = lambdaFns.filter(
    (lambdaFn) => lambdaFn.cwEventRuleNames.length > 0
  )
- console.log(batchLambdaFns)

+ fs.writeFileSync(
+   'batch-lambda-fns.md',
+   markdownTable(
+     [['Lambda ARN', 'Event rule name']].concat(
+       batchLambdaFns.map((batchLambdaFn) => [
+         batchLambdaFn.lambdaFnArn,
+         batchLambdaFn.cwEventRuleNames.join(','),
+       ])
+     )
+   )
+ )
})()

実行すると batch-lambda-fns.md ファイルを出力します。内容は以下のようになります。

| Lambda ARN                                                             | Event rule name |
| ---------------------------------------------------------------------- | --------------- |
| arn:aws:lambda:ap-northeast-3:AWS_ACCOUNT_ID:function:sample-batch-fn-6  | sample-rule-6   |
| arn:aws:lambda:ap-northeast-3:AWS_ACCOUNT_ID:function:sample-batch-fn-20 | sample-rule-20  |
| arn:aws:lambda:ap-northeast-3:AWS_ACCOUNT_ID:function:sample-batch-fn-5  | sample-rule-5   |
| arn:aws:lambda:ap-northeast-3:AWS_ACCOUNT_ID:function:sample-batch-fn-0  | sample-rule-0   |
...(省略)...

以上でやりたいことができました。参考になれば幸いです。

ちなみに: CloudWatch EventsとAmazon EventBridge

ところで、CloudWatch Eventsについて調べていると、ドキュメントなどで「Amazon EventBridge (CloudWatch Events)」のように表記されるのを見ます。

Amazon EventBridge (CloudWatch Events) でAWS Lambdaを使用する - AWS Lambda

FAQでは、CloudWatch EventsとAmazon EventBridgeの関係について以下のように説明されています。

よくある質問 - Amazon EventBridge | AWS

Q: Amazon EventBridge は CloudWatch Events とどのように関連していますか? Amazon EventBridge は、CloudWatch Events をベースに構築された、CloudWatch Events を拡張するサービスです。Amazon EventBridge では、CloudWatch Events と同じサービス API とエンドポイント、同じ基盤となるサービスインフラストラクチャを使用します。これまで CloudWatch Events を使用しているお客様にとっては、何も変わることはありません。これまでと同じ API、CloudFormation テンプレート、コンソールを引き続き使用できます。お客様からの報告によれば、CloudWatch Events はイベント駆動型アーキテクチャを構築するための理想的なサービスです。そのため、AWS ではお客様が独自のアプリケーションとサードパーティーの SaaS アプリケーションからデータを接続できるようにする新しい機能を構築しました。AWS は、この機能を CloudWatch サービス内にとどめておくのではなく、Amazon EventBridge という新しい名前でリリースしました。この機能が、CloudWatch Events が開発された目的であるモニタリングユースケースを超えた拡張であることを示すためです。

実際、今回作成したCloudWatch EventsルールのリソースをAWSマネジメントコンソールから確認してみます。

まず、CloudWatch→「イベント」→「ルール」で確認してみます。

続いて、Amazon EventBridge→「イベント」→「ルール」で確認してみます。

どちらから見ても同じようにリソースを確認できるようになっているようです。

参考