📚

ESLint プラグインを使用して CDK のセオリーを適用する

2024/12/01に公開

『AWS CDK Advent Calendar 2024』1 日目の記事です。

https://qiita.com/advent-calendar/2024/aws-cdk

はじめに

AWS CDK を使った開発で、以下のようなケースに遭遇したことはありませんか?

  • チームのメンバー間でコーディング規則(書き方)が統一されていない
  • レビューで毎回同じ指摘をしている
  • CDK コーディングにおけるセオリー・Tips を意識しながらのコーディングが大変

本記事では、これらの課題を解決する ESLint プラグインを紹介します。

ESLint とは?

そもそも、ESLint とは何でしょうか?

ESLint は、JavaScript や TypeScript などのコードを解析し、単純な構文エラーやコーディング規則に違反するコードを検出するツールです。

このツールを使用することで、チームのメンバー間でコーディング規則(書き方)を統一することができます。

コーディング規則には、ESLint から提供される推奨ルールを適用することもできますし、世の中に出回っている ESLint プラグイン(typescript-eslint-pluginなど)を使用して、ESLint ルールを拡張することもできます。(以下、設定ファイルの例)

eslint.config.js
import eslint from "@eslint/js";

export default [
    eslint.configs.recommended,
];

また、ESLint では簡単に(自作で)独自のカスタムルールを作ることができるので、チーム内で独自のルールを定義し、それを適用することで、より厳格にチームのコーディング規則を定義することもできます。

CDK で ESLint を使用する旨味

ここまで ESLint の概要を解説しましたが、CDK で ESLint を使用する旨味とは何でしょうか?

もちろん、CDK で開発を行う際にはコーディングをするので、一般的な(ESLint から提供されるような)コーディング規則が適用され、より適切な TypeScript コードを書けることは旨味の一部であると思います。
しかし、これはどちらかというと TypeScript ユーザーにとっての旨み という表現の方が近い気がします。

私は CDK で ESLint を使用する旨味は、一般化された CDK コーディングにおけるセオリーをコーディング規則として取り入れられること だと考えています。

例えば、Construct ID に関するセオリーだと、以下の記事が参考になります

https://dev.classmethod.jp/articles/best-way-to-name-aws-cdk-construct-id/

こちらの記事では、以下のようなセオリーが取り上げられています(一部)

  • Construct ID は scope の中で一意にする
  • Construct ID は PascalCase で記述する
  • Construct ID に ConstructStack とつけない
  • 親 コンストラクト で表している情報を繰り返さない


また、再利用性に関するセオリーだと、以下のスライドが参考になります

https://speakerdeck.com/gotok365/aws-cdk-reusability

こちらのスライドでは、以下のようなセオリーが取り上げられています(一部)

  • Props のプロパティには readonly をつける
  • Props や Construct で公開するリソースのプロパティは IXxx 型にする(Class 型にしない)
  • Props や Construct で公開するプロパティにプリミティブ型は避ける


熟練の CDK ユーザー / TypeScript ユーザーであれば、これらのセオリーを意識したコードを書くことは難しくないかもしれません。
しかし、CDK や TypeScript にまだ慣れていないエンジニアがこれらのセオリーを一つ一つ意識してコードを書くことは、難しそうに感じます。

そこで、これらのセオリーを ESLint のルールに落とし込むことで、自動的な指摘・修正を実現でき、CDK 開発をより豊かにできると考えました。

また、チーム内でこれらのセオリーをコーディング規則として提供してあげることで、CDK 初学者でも勝手にセオリーを意識したコードを書くことができ、レビューコストの削減につながると考えました。

eslint-cdk-plugin の誕生

そこで、先ほど紹介したような CDK のセオリーを強制できる ESLint プラグインを作成しました!

https://eslint-cdk-plugin.dev/

プラグインの内容

このプラグインでは、以下のようなルールを適用することができます。
これにより、先ほど紹介した記事内で書かれている(一部)セオリーを、意識せずに適用することができます!!


命名規則に関するルール

✨ Construct ID は PascalCase で書く

CloudFormation により自動で命名されるリソースの名前を見やすくするため、Construct ID は PascalCase で記述することが推奨されます。

参考: https://qiita.com/tmokmss/items/721a99e9a62499d6d54a

このルールでは、Construct ID を PascalCase で書くことを強制できます

✅ Good

const bucket = new Bucket(this, "MyBucket");

❌ Bad

const bucket = new Bucket(this, "myBucket");


🚫 Construct ID に "Construct" や "Stack" などの suffix をつけない

Construct ID に "Construct" や "Stack" が含まれていると、CDK の世界で止めるべき問題が CloudFormation テンプレートおよび AWS の世界に漏れてしまうため、これらの suffix はつけないことが推奨されます。

参考: https://dev.classmethod.jp/articles/best-way-to-name-aws-cdk-construct-id/#construct-id-%25E3%2581%25ABconstruct%25E3%2582%2584stack%25E3%2581%25A8%25E3%2581%25A4%25E3%2581%2591%25E3%2581%25AA%25E3%2581%2584

このルールでは、Construct ID および Stack ID で "Construct" および "Stack" suffix の使用を禁止することができます

✅ Good

const bucket = new Bucket(this, "MyBucket");

❌ Bad

const bucket = new Bucket(this, "BucketConstruct");


🔄 Construct ID に 親 Construct の名前をつけない

Construct ID に親 Construct の Class 名と一致する文字列を指定すると、CloudFormation リソースの階層が不明瞭になるため、親 Construct の名前を付けないことが推奨されます。

このルールでは、親 Construct の名前(Class 名)を Construct ID として使用することを禁止できます。

✅ Good

export class MyConstruct extends Construct {
  constructor(scope: Construct, id: string) {
    super(scope, id);
    const bucket = new Bucket(this, "MyBucket");
  }
}

❌ Bad

export class MyConstruct extends Construct {
  constructor(scope: Construct, id: string) {
    super(scope, id);
    const bucket = new Bucket(this, "MyConstruct");
  }
}



型安全性に関するルール

🔒 Props(Interface)のプロパティには"readonly"をつける

Construct の入力である Props の誤った上書きを避けるため、Props のプロパティは readonly にすることが推奨されています

参考: https://speakerdeck.com/gotok365/aws-cdk-reusability?slide=14

このルールでは、Props のプロパティにreadonlyをつけることを強制できます

✅ Good

interface MyConstructProps {
  readonly bucket: IBucket;
}

❌ Bad

interface MyConstructProps {
  bucket: IBucket;
}


🔒 Class の public 変数には"readonly"をつける

Class のパブリック変数が変更可能である場合、意図しない副作用が発生する可能性があるため、public 変数 には readonly をつけることが推奨されます

このルールでは、public 変数にreadonlyをつけることを強制できます

✅ Good

export class MyConstruct extends Construct {
  public readonly bucket: IBucket;
}

❌ Bad

export class MyConstruct extends Construct {
  public bucket: IBucket;
}


📦 Props のプロパティに Class 型を指定しない

Props (Interface)のプロパティに Class を使用すると、Props と Class の間に密接な結合が作成されます。
さらに、Class は本質的に変更可能であるため、Props のプロパティとして Class を使用すると予期しない動作が発生する可能性があります
そのため、Props のプロパティには Class 型ではなく Interface 型(IXxx型) を指定することが推奨されます。

参考: https://speakerdeck.com/gotok365/aws-cdk-reusability?slide=21

このルールでは、Props のプロパティに Class 型を指定することを禁止できます。

✅ Good

import { IBucket } from "aws-cdk-lib/aws-s3";

interface MyConstructProps {
  bucket: IBucket;
}

❌ Bad

import { Bucket } from "aws-cdk-lib/aws-s3";

interface MyConstructProps {
  bucket: Bucket;
}


📦 Class の public 変数に Class 型を指定しない

Class の public 変数も Props と同様に、Class 型を指定しないことが推奨されます。
(Class 型を使用すると、密結合が作成され、可変状態が公開されるため)

このルールでは、public 変数に Class 型を指定することを禁止できます。

✅ Good

import { IBucket } from "aws-cdk-lib/aws-s3";

class MyConstruct extends Construct {
  public readonly bucket: IBucket;
}

❌ Bad

import { Bucket } from "aws-cdk-lib/aws-s3";

class MyConstruct extends Construct {
  public readonly bucket: Bucket;
}



モジュール構成に関するルール

🚧 private ディレクトリ内のモジュールを、外部から呼び出さない

privateディレクトリは、親ディレクトリ内でのみ使用される内部実装を格納することを目的としています。
異なる階層からのインポートを禁止することで、適切なモジュール化とカプセル化を促進します。

このルールでは、異なる階層レベルの private ディレクトリからのモジュールのインポートを禁止できます。

✅ Good

lib/constructs/my-construct.ts
import { MyConstruct } from "./private/my-construct";

❌ Bad

lib/constructs/my-construct.ts
import { MyConstruct } from "../private/my-construct";
import { MyConstruct } from "../my-app/private/my-construct";


プラグインの導入手順

eslint-cdk-pluginの導入手順は以下の通りです

1. プラグインのインストール

# npm
npm install --save-dev eslint-cdk-plugin

# yarn
yarn add -D eslint-cdk-plugin

# pnpm
pnpm install -D eslint-cdk-plugin

2. eslint.config.mjs の設定

eslint.config.mjs
import eslint from "@eslint/js";
import tsEslint from "typescript-eslint";
+ import eslintCdkPlugin from "eslint-cdk-plugin";

export default [
  // ESLintの推奨設定を適用
  eslint.configs.recommended,
  ...tsEslint.configs.recommended,
  ...tsEslint.configs.stylistic,
  {
    // lib と bin ディレクトリ配下の typescript ファイルを対象にする
    files: ["lib/**/*.ts", "bin/*.ts"],
    languageOptions: {
      parserOptions: {
        projectService: true,
        project: "./tsconfig.json",
      },
    },
    plugins: {
+     cdk: eslintCdkPlugin,
    },
    rules: {
+     ...eslintCdkPlugin.configs.recommended.rules,
    },
  },
  // 無視するディレクトリを指定
  {
    ignores: ["cdk.out", "node_modules", "*.js"],
  }
];

または、以下のような書き方も可能です
(こちらの書き方は型補完が出るため、私はこちらの書き方を推奨します)

eslint.config.mjs
import eslint from "@eslint/js";
import tsEslint from "typescript-eslint";
+ import eslintCdkPlugin from "eslint-cdk-plugin";

export default tsEslint.config(
  // ESLintの推奨設定を適用
  eslint.configs.recommended,
  ...tsEslint.configs.recommended,
  ...tsEslint.configs.stylistic,
  {
    // ...省略
    plugins: {
+     cdk: eslintCdkPlugin,
    },
    rules: {
+     ...eslintCdkPlugin.configs.recommended.rules,
    },
  },
  // ...省略
);


そのほか、詳しい情報は以下のドキュメントをご覧ください

まとめ

本記事では、AWS CDK のコーディングにおけるベストプラクティスを自動的にチェックできる ESLint プラグイン eslint-cdk-plugin を紹介しました。

今後も、アプリエンジニアのCDKユーザー としてコミュニティーへの貢献を目指していきます!

フィードバックお待ちしています

私はこのプラグインは継続的にメンテナンスしており、新しいルールの追加や機能の改善を行っています。
しかし、私はまだまだ CDK の知識・経験が浅く、知らないセオリーや Tips がたくさんあります・・・
そのため、新しいルールの提案などがありましたら、ぜひコメントください!!
(このプラグインは OSS として公開しておりますので、Issue を作成いただくこともできます!)

https://github.com/ren-yamanashi/eslint-cdk-plugin/issues

より良い CDK 開発環境の構築に向けて、みなさまのご意見をお待ちしております!

Discussion