AWS CDK: 非同期処理/async 関数を呼ぶ

概要

AWS CDK では通常クラスのコンストラクタでリソースを生成します。ただし単純にコンストラクタを async にすることができないため、非同期関数は使えません。非同期関数を使う方法について整理してみました。

実装

例として、AWS SDK を使って現存するすべての Lambda 関数を取得し、それらの Alarm を生成してみます。

案1 : 非同期処理で結果を取得して、props で渡す

// lib/AppStack.ts
import * as AWS from "aws-sdk";
import * as cdk from "aws-cdk-lib";
import * as cloudwatch from "aws-cdk-lib/aws-cloudwatch";
import * as lambda from "aws-cdk-lib/aws-lambda";
import {Construct} from "constructs";

// AppStack に渡す Props
interface AppStackProps extends cdk.StackProps {
    readonly functions: string[];
}

export class AppStack extends cdk.Stack {
    constructor(scope: Construct, id: string, props: AppStackProps) {
        // Stack を生成
        super(scope, id, props);

        // Stack (this) に紐付いたリソースを生成
        props.functions.forEach(functionName => {
            new cloudwatch.Alarm(this, `Alarm-Errors-${functionName}`, {
                alarmName: `Errors-${functionName}`,
                comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,
                datapointsToAlarm: 1,
                evaluationPeriods: 1,
                metric: lambda.Function.fromFunctionName(this, functionName, functionName).metricErrors({
                    period: cdk.Duration.seconds(60),
                }),
                threshold: 0,
                treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING,
            });
        })
    }
}

export async function createAppStack(scope: Construct, id: string, props: cdk.StackProps): Promise<cdk.Stack> {
    // 非同期処理
    // ※注意: ListFunctions はいちどに 50 個までしか取得できないため、それより多い場合はこれだけでは不足
    const res = await new AWS.Lambda().listFunctions().promise();
    const functions = res.Functions?.map(f => f.FunctionName!) || [];

    return new AppStack(scope, id, Object.assign({functions}, props));
}

これをメインのスクリプトから呼びます。

// bin/app.ts
import * as cdk from "aws-cdk-lib";
import {createAppStack} from "../lib/AppStack";

process.on('unhandledRejection', (e, promise) => {
    console.error(e);
    process.exit(1);
});

(async () => {
    const app = new cdk.App();
    await createAppStack(app, "AppStack", {});
})();

補足として、Node.js v14 までのデフォルトでは例外が発生してもプロセスは正常終了してデプロイが走ってしまうので、process.exit(1); で明示的にプロセスをエラー終了させています。最新版を使うのであれば不要かと思います。

案2 : 先に Stack だけ生成して、各リソースを追加していく

案1 は先にすべての非同期処理をおこなって props に詰めて渡すという手間があるので、Stack のコンストラクタ内でリソースを生成するのをやめて、先に Stack だけ生成して、各リソースを追加していっても良さそうです。

// lib/AppStack.ts
import * as AWS from "aws-sdk";
import * as cdk from "aws-cdk-lib";
import * as cloudwatch from "aws-cdk-lib/aws-cloudwatch";
import * as lambda from "aws-cdk-lib/aws-lambda";
import {Construct} from "constructs";

export async function createAppStack(scope: Construct, id: string, props: cdk.StackProps): Promise<cdk.Stack> {
    // Stack を生成 : cdk.Stack を直接インスタンス化する。
    const stack = new cdk.Stack(scope, id, props);

    // 非同期処理
    const res = await new AWS.Lambda().listFunctions().promise();
    const functions = res.Functions?.map(f => f.FunctionName!) || [];

    // Stack (stack) に紐付いたリソースを生成
    functions.forEach(functionName => {
        new cloudwatch.Alarm(stack, `Alarm-Errors-${functionName}`, {
            alarmName: `Errors-${functionName}`,
            comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,
            datapointsToAlarm: 1,
            evaluationPeriods: 1,
            metric: lambda.Function.fromFunctionName(this, functionName, functionName).metricErrors({
                period: cdk.Duration.seconds(60),
            }),
            threshold: 0,
            treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING,
        });
    })

    return stack;
}

メインのスクリプトは案 1 と同様です。