🎄

CDK in TypeScriptでのimport文の書き方を比較してみる

2023/12/20に公開

はじめに

こんにちは。AlphaDrive で Web アプリケーションエンジニアをしているkeyaminです。

今回は AWS CDK に関するちょっと細かい話をしたいと思います。

みなさんは CDK in TypeScript でコードを書くとき、import 文をどのように書きますか?

CDK v2 ではaws-cdk-libというパッケージにすべての AWS サービス用の Construct などが含まれており、個別にパッケージを追加しなくてもいいようになっています。

しかし、ファイル先頭で import 文を書くときの流派はチームによって異なるのではないでしょうか。

今回はいくつかの方法を簡単に比較したいと思います。

題材とするコードでは、SQS キューとデッドレターキュー、SNS トピック、デッドレターキューにキューが入ると SNS トピックにメッセージを送信する CloudWatch アラームを作成しています。

import * as cdk from "aws-cdk-lib"ですべてを解決する

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

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

    const dlq = new cdk.aws_sqs.Queue(this, "DeadLetterQueue", {
      queueName: "dead-letter-queue",
    });

    dlq
      .metricApproximateNumberOfMessagesVisible()
      .createAlarm(this, "QueueAlarm", {
        alarmName: "queue-alarm",
        threshold: 1,
        evaluationPeriods: 1,
      })
      .addAlarmAction(
        new cdk.aws_cloudwatch_actions.SnsAction(
          new cdk.aws_sns.Topic(this, "Topic", {
            topicName: "alarm-topic",
          })
        )
      );

    new cdk.aws_sqs.Queue(this, "Queue", {
      queueName: "queue",
      visibilityTimeout: cdk.Duration.seconds(300),
      deadLetterQueue: {
        queue: dlq,
        maxReceiveCount: 1,
      },
    });
  }
}

これは少数派かなと思いますが、1 文ですべてを持ってくる方法です。

メリット

  • import 文が最も少なくすむ
  • コードを書きながら欲しい Construct や定数を探すとき、とりあえずcdk.と入力すればエディタの補完から探せる

デメリット

  • ファイルの先頭をパッと見たとき、どの AWS サービスのリソースが作成されているのか分かりづらい
  • 記述が冗長になる(サンプルではcdk.aws_cloudwatch_actions.SnsActionが目立ちますね)

import { aws_sqs } from "aws-cdk-lib"のように名前付きインポートする

import { aws_cloudwatch_actions, aws_sns, aws_sqs } from "aws-cdk-lib";
import * as cdk from "aws-cdk-lib";
import { Construct } from "constructs";

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

    const dlq = new aws_sqs.Queue(this, "DeadLetterQueue", {
      queueName: "dead-letter-queue",
    });

    dlq
      .metricApproximateNumberOfMessagesVisible()
      .createAlarm(this, "QueueAlarm", {
        alarmName: "queue-alarm",
        threshold: 1,
        evaluationPeriods: 1,
      })
      .addAlarmAction(
        new aws_cloudwatch_actions.SnsAction(
          new aws_sns.Topic(this, "Topic", {
            topicName: "alarm-topic",
          })
        )
      );

    new aws_sqs.Queue(this, "Queue", {
      queueName: "queue",
      visibilityTimeout: cdk.Duration.seconds(300),
      deadLetterQueue: {
        queue: dlq,
        maxReceiveCount: 1,
      },
    });
  }
}

これは採用している方も多いのではないでしょうか。Durationのようなサービスによらないものはcdk.で呼び出しつつ、他をシンプルに名前付きインポートすることで、頭にcdk.を付ける必要がなくなり、冗長な感じが緩和されます。使用しているサービスも import 文を見ればわかるので、バランスが良い書き方ですね。ただ、aws_の部分がまだ若干冗長なのと、aws_cloudwatch_actionsといった長めのパッケージはまだ目立ちます。

メリット

  • ファイルの先頭を見ると使用している AWS サービスがわかる
  • 記述がすこし短くなる

デメリット

  • aws_がまだ少し冗長

import { aws_sqs as sqs } from "aws-cdk-lib"のように名前を変更してインポートする

import * as cdk from "aws-cdk-lib";
import {
  aws_cloudwatch_actions as cw_actions,
  aws_sns as sns,
  aws_sqs as sqs,
} from "aws-cdk-lib";
// こっちでも可
// import * as cw_actions from "aws-cdk-lib/aws-cloudwatch-actions";
// import * as sns from "aws-cdk-lib/aws-sns";
// import * as sqs from "aws-cdk-lib/aws-sqs";
import { Construct } from "constructs";

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

    const dlq = new sqs.Queue(this, "DeadLetterQueue", {
      queueName: "dead-letter-queue",
    });

    dlq
      .metricApproximateNumberOfMessagesVisible()
      .createAlarm(this, "QueueAlarm", {
        alarmName: "queue-alarm",
        threshold: 1,
        evaluationPeriods: 1,
      })
      .addAlarmAction(
        new cw_actions.SnsAction(
          new sns.Topic(this, "Topic", {
            topicName: "alarm-topic",
          })
        )
      );

    new sqs.Queue(this, "Queue", {
      queueName: "queue",
      visibilityTimeout: cdk.Duration.seconds(300),
      deadLetterQueue: {
        queue: dlq,
        maxReceiveCount: 1,
      },
    });
  }
}

こちらも採用している方が多そうな書き方です。AWS CDK Workshop でもこの書き方でサンプルコードが提供されており、BLEA でもスタンダードになっていますね。自分たちの好きな別名をつけれることで、冒頭のaws_が省略できる他、cloudwatch_actionscw_actionsとしたり、stepfunctionssfnとしたりできてコードがスッキリします。略語の流派がチームメンバーによって違うとケンカになりそう(例えば EventBridge を CloudWatch Events から cwe とするか EventBridge から eb とするか、など)なのと、新規でコードを書く際はエディタの補完が効かないのでパッケージの追加ごとにファイル先頭に戻って import 文に追加する必要があるのが面倒です...

メリット

  • 好きな別名を付けられるのでコードがスッキリする
  • BLEA でスタンダードな書き方なので、コーディング規約的にも「BLEA の書き方に準ずる」としておけば良い

デメリット

  • 略語で混乱するケースがある
  • ファイル内で新しいサービスを使用する際、一旦ファイル先頭に戻って import を追加する必要がある

import { Queue } from "aws-cdk-lib/aws-sqs"のように AWS サービスごとに名前付きインポートする

import { Duration, Stack, StackProps } from "aws-cdk-lib";
import { SnsAction } from "aws-cdk-lib/aws-cloudwatch-actions";
import { Topic } from "aws-cdk-lib/aws-sns";
import { Queue } from "aws-cdk-lib/aws-sqs";
import { Construct } from "constructs";

export class SampleStack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);

    const dlq = new Queue(this, "DeadLetterQueue", {
      queueName: "dead-letter-queue",
    });

    dlq
      .metricApproximateNumberOfMessagesVisible()
      .createAlarm(this, "QueueAlarm", {
        alarmName: "queue-alarm",
        threshold: 1,
        evaluationPeriods: 1,
      })
      .addAlarmAction(
        new SnsAction(
          new Topic(this, "Topic", {
            topicName: "alarm-topic",
          })
        )
      );

    new Queue(this, "Queue", {
      queueName: "queue",
      visibilityTimeout: Duration.seconds(300),
      deadLetterQueue: {
        queue: dlq,
        maxReceiveCount: 1,
      },
    });
  }
}

この書き方ですと先頭のcdk.aws_sqs.がまるごとなくなり、コード量的には最も少なくなります。
ただ、1 つずつ名前付きインポートしているので import 文が肥大化しがちだったり、ものによってはどのサービスから import されているクラスなのかがわかりづらかったりします(aws_stepfunctions_tasks のクラスなど)

メリット

  • メソッドチェーンがほぼなくなり、もっともコードがスッキリする

デメリット

  • import 文が長くなりがち
  • どのサービスから import されているものなのか、分かりづらいことも

まとめ

それぞれの書き方にメリット・デメリットはありますが、それを理解した上で統一した書き方がされていればどの方法でも良いかと思います。
自分のチームではファイルごとに書き方がバラバラだったため、最近 3 つめの方法に統一しました。CDK コードの規約まで作っているチームもなかなかないと思いますので、「とりあえず BLEA っぽく書く」のを規約とする雑に強い運用もできておすすめです。
また CDK はプログラミング言語のベストプラクティスや規約をそのまま持ってきやすいのもナイスなところですね!
本記事が皆さまの CDK ライフの一助となれば幸いです。ありがとうございました 🙌

おまけ

上の 2 つめの書き方で開発しているとき、VSCode で新しいサービスの import を補完から追加すると...

VSCodeの入力補完

cdkが頭についている

aws-cdk-libから名前付きインポートしたいのにcdk.xxxとなってしまい 😡 となったこと、ないでしょうか?

そんなときはimport * as cdk from "aws-cdk-lib"import * as cdk from "aws-cdk-lib/core"としてあげるとaws-cdk-libからのインポートに加えてくれます。

cdk.から使用したいのはRemovalPolicyDurationなどのサービスによらないものだと思います。それらはcoreで提供されており、import をそこに絞ってあげている感じです。

誤ってサービス固有のものをcdk.で呼び出すのを防止するためにもおすすめです!

GitHubで編集を提案

Discussion