5
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

LambdaのCDKバンドルから逃げたい話

Last updated at Posted at 2025-03-05

背景

みなさん、TypeScriptのCDKプロジェクトで同じTypeScriptを使うLambdaを追加する際に、なんとなくこういうやり方なんでしょうか。

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

    const fn_bundlebycdk = new NodejsFunction(this, 'bundlebycdk', {
      entry: 'lambda-bundle-by-cdk/index.ts',
      handler: 'handler',
      runtime: Runtime.NODEJS_20_X,
    });
  }
}

image.png

入口のTypeScriptファイル(NodejsFunctionentryパラム)を指定すれば、CDKが良しなにやってくれて助かります。Lambda関数のフォルダ配下にも、独立なPackageを作らずに済みます。
単純な処理やDependenciesもそんなに多くない場合なら、それでよいと思います。小さいところまでパッケージを切り出すとプロジェクト構成が複雑になりますから。

しかし、こうすると一つ最大な欠点が生じかねません:インフラを管理するCDKプロジェクトと振る舞いを管理するLambda関数が同じパッケージの下につく。

そのため、以下のようなこともあり得ます

  • tsconfig.jsonも共有してしまう。設定いじらないと動かない場合、これなんのために設定変えたの?というのを場合によってインフラ側の責任者に説明しないといけないし、そもそもよくないやり方だ
  • package.jsonも同様な理由で、いっぱいDependenciesを導入すると、これインフラのため?Lambdaのため?って混乱を招きかねない

記事内のコードは、こちらのレポジに収納しました。

ランタイムの切り出し

やることは、まずランタイムの切り出しです。

mkdir lambda-package
npm init
npx tsc --init

上記のコマンドでもう一つ新しいフォルダを作って、それをNPMパッケージ化します。
そうすると、CDKプロジェクトと違うtsconfig.tsを搭載できます。公式のCDK boilerplateだと、デフォルトのtarget設定はCommonJSになっていますので、それをES modulesに変えるだけでも結構助かります。デコレーターの有効化設定など、CDKプロジェクトに入れちゃうと意味不明のものを独立のパッケージ内でやる方がよいでしょう。package.jsonにも、上層のCDKプロジェクトに入っていないDependenciesを入れられます。

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

    const fn_package = new NodejsFunction(this, 'package', {
      entry: 'lambda-package/src/index.ts',
      handler: 'handler',
      runtime: Runtime.NODEJS_20_X,
    });
  }
}

このように、同じやり方でデプロイできます。

しかし、ここまでやっても、結局CDKに任しているような状況だと言わざるを得ないのです。

CDKバンドル

CDKのConstructーーNodejsFunctionentryパラムを指定すると、何が起こるのでしょう。
もちろん、公式ドキュメントに明記しております:

class NodejsFunction (construct)
A Node.js Lambda function bundled using esbuild.

esbuildを使うバンドルを行うのです。一つの入口を指定すれば、esbuildはそれを起点にして、関連ファイルを全部一つのindex.jsに整合して、CDKがそれをLambda環境に上がります。

CDK+esbuildの余談ですが、

CDKは既存環境でesbuildを探して、それがない場合Dockerを起動して、Docker内でバンドルをやろうとします(遅い)。両方ない場合は失敗しますので、CDKプロジェクトを生成する前に、

npm i -D esbuild

で、ローカル環境のesbuildを確保したほうがスムーズになります。

本筋に戻ります。一見何の問題もない、スマートなやり方ですが、場合によって不具合が生じます。

まず、tscによるタイプチェックやトランスパイルが行われないことです。これ自体が普通で、現在(2025年3月)tscを使うトランスパイルに執着する必要がそもそも薄く、esbuildなどとにかく早いツールに世の中が乗り換えています。tscによる動的なタイプチェックが必要の場合、開発環境でtscを使ってチェックすればよいかと、多くの人はそう思っています。しかし、それが100%通用するとは言えません。

筆者は昔TypeORMをLambda関数で使いたいシーンに出会いましたが、esbuildがトランスパイル中にデコレーター情報をはがしてしまうため、tscによるトランスパイルが必要でした。それをCDKのバンドルプロセスで実現するworkaround(回避策)を記事で紹介しました。当時はこんな感じでLambdaを作成しました:

    const fn = new NodejsFunction(this, "typeorm-lambda", {
      //...
      bundling: {
        preCompilation: true, 
        esbuildArgs: {
          "--resolve-extensions": ".js",
        },
      },
    });

ここのpreCompilationは、esbuildバンドルの前にtscトランスパイルすることでデコレーター情報を守るためです。中身の振る舞いとしては、

まず

tsc "...\index.ts"
    --alwaysStrict
    ...[options]...
    --outDir ./
    --rootDir ./
    ...

tscコマンドで目標ファイル(入口ファイル)指定でtscを実行します。

そして

    esbuild 
    --bundle "...\index.js"
    --target=node20
    --platform=node
    --outfile="...\cdk.out\...\index.js"
    --external

esbuildコマンドでバンドルを行います。

このプロセスの欠点を申し上げますと、tscコマンドはフォルダ内のtsconfigを一部適用しない場合があります。こちらのissueによると、少なくとも現時点ではoutDirが指定できないような状況です。

筆者の推測

普通、tscコマンドをファイル指定で実行する場合、フォルダ内のtsconfigは適用されませんが、CDKのpreCompilationは多分ちゃんとtsconfigを見に行って、大体の設定をそのままコマンドのオプションに適用して、一部の設定(outDirなど)を強制指定しています。

せっかくパッケージとして切り出してほしい設定をいろいろ入れてみましたが、これだとモヤモヤが残りますね。

そこで、CDKから主導権を奪いたいと思いました。中身のプロセスも分かった前提なら、それを真似してプロセスをコントロールの下に置くっというのはいかがでしょうか。

バンドルの切り出し

mkdir lambda-mybundle
npm init
npx tsc --init

前々節と同様にNPMパッケージを新に作ります。ちょっと違うのは、tsconfigoutDirパラムを./distに指定して、package.jsonでいくつかスクリプトを追加します:

{
  "scripts": {
    "build": "tsc",
    "bundle": "esbuild --bundle dist/index.js --outfile=bundle/index.js --platform=node --target=node20",
    "produce": "npm-run-all2 build bundle"
  },
}

CDKでは、こういう風にLambdaを作成します

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

    execSync('npm i && npm run produce', {
      cwd: 'lambda-mybundle',
      stdio: 'inherit',
      shell: 'bash',
    });

    const fn_mybundle = new NodejsFunction(this, 'mybundle', {
      code: Code.fromAsset('lambda-mybundle/bundle'),
      handler: 'index.handler',
      runtime: Runtime.NODEJS_20_X,
    });
  }
}

ざっくり説明しますと、

  1. Lambdaを作成する前に、チャイルドプロセスで前に作成したパッケージでトランスパイル・バンドルを行う
  2. 入口ファイル指定じゃなくて、package.jsonのスクリプトで作成したアセットをLambda関数の中身として指定

これで、CDKと一切関係せずにLambdaのバンドルを行えます。

最後に

CDK?ランタイム?パッケージ?tsconfig?いろんな❔マークが頭に浮かびながらAWSアプリケーション作成を始めた初心者なりに、ようやくちょっと理解し始めたなあと思ったので、記事に残ります。

少しでもご参考になれば幸いです。

5
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?