[アップデート]AWS SAM CLIでnodejsをビルドする際に元々のソースディレクトリを参照しビルドができるようになりました

埋もれていきそうな気がしたので紹介させていただきましたが、自分もよくわかってないので有識者の方いましたら良いところをご教示いただけますと非常に助かります。
2023.12.08

初めに

昨日、AWS SAM CLIのv1.104.0がリリースされました。

今回のソースコードを格納しているディレクトリを参照しでビルドを行うオプションである--build-in-sourceオプションが追加されたようです。

PRに詳しいコメントや関連issuesの記載がなくソースコードの変更や実際の動作を書くにする限りの挙動のため本来意図しているものかは不明ですが、こちらのオプションを利用するとビルドが一部リソースが元々のソースディレクトリ内を参照するようになりこれにより大量の依存ライブラリが含まれる環境におけるビルド時間の短縮等が見込めるようになりそうです。

    --build-in-source / --no-build-in-source
                      Opts in to build project in the source folder. The following workflows support
                      building in source: ['nodejs12.x', 'nodejs14.x', 'nodejs16.x', 'nodejs18.x',
                      'Makefile', 'esbuild']

現在はnodejs周りのみがサポートされており実際の動作を確認する限りnode_modulesフォルダが対象となりました(現時点ではnodejs20.xは対象外のようです)。

sam buildの仕組みについて

まず前提知識としてsam buildの仕組みの一部を理解する必要があります。

以前別の記事で紹介させていただきましたがSAMでビルドを行う際にはソースコードが格納されているディレクトリで直接ビルドするのではなく、一旦別のディレクトリにコピーしそれを対象としビルドする仕組みとなっております。

この辺りの具体的な処理はランタイムにより異なるのですがnodejsの場合は以下のような処理が行われます。

  1. 一時フォルダにnpm packnpm unpackedを利用して不要なものもを取り除いた上で展開する
  2. 最終的なsamのビルドの成果物が格納される各プロジェクト内の.aws-sam/build内に移す
  3. npm installにより各種ライブラリをインストールする
$ sam build --no-cached --debug 
...
2023-12-08 01:05:24,821 | NODEJS packaging file:/Users/xxxx/sam-app-node/hello-world to /var/folders/8m/b717gxmx46l7ytmt2smb507w0000gp/T/tmp262yxsqf                       
2023-12-08 01:05:24,821 | executing NPM: ['npm', 'pack', '-q', 'file:/Users/xxxx/sam-app-node/hello-world']                                                                
2023-12-08 01:05:25,040 | NODEJS packed to hello_world-1.0.0.tgz                                                                                                                       
2023-12-08 01:05:25,040 | NODEJS extracting to /var/folders/8m/b717gxmx46l7ytmt2smb507w0000gp/T/tmp262yxsqf/unpacked                                                                   
2023-12-08 01:05:25,041 | NodejsNpmBuilder:NpmPack succeeded                                                                                                                           
2023-12-08 01:05:25,042 |  Running NodejsNpmBuilder:CopyNpmrcAndLockfile                                                                                                               
2023-12-08 01:05:25,042 | NodejsNpmBuilder:CopyNpmrcAndLockfile succeeded                                                                                                              
2023-12-08 01:05:25,042 |  Running NodejsNpmBuilder:CopySource                                                                                                                         
2023-12-08 01:05:25,043 | Creating target folders at /Users/xxxx/sam-app-node/.aws-sam/build/HelloWorldFunction                                                            
2023-12-08 01:05:25,043 | Copying directory metadata from source (/var/folders/8m/b717gxmx46l7ytmt2smb507w0000gp/T/tmp262yxsqf/unpacked/package) to destination                        
(/Users/xxxx/sam-app-node/.aws-sam/build/HelloWorldFunction)                                                                                                               
2023-12-08 01:05:25,044 | Copying source file (/var/folders/8m/b717gxmx46l7ytmt2smb507w0000gp/T/tmp262yxsqf/unpacked/package/package.json) to destination                              
(/Users/xxxx/sam-app-node/.aws-sam/build/HelloWorldFunction/package.json)                                                                                                  
2023-12-08 01:05:25,044 | Copying source file (/var/folders/8m/b717gxmx46l7ytmt2smb507w0000gp/T/tmp262yxsqf/unpacked/package/app.mjs) to destination                                   
(/Users/xxxx/sam-app-node/.aws-sam/build/HelloWorldFunction/app.mjs)                                                                                                       
2023-12-08 01:05:25,045 | NodejsNpmBuilder:CopySource succeeded                                                                                                                        
2023-12-08 01:05:25,045 |  Running NodejsNpmBuilder:NpmInstall                                                                                                                         
2023-12-08 01:05:25,045 | NODEJS installing in: /Users/xxxx/sam-app-node/.aws-sam/build/HelloWorldFunction                                                                 
2023-12-08 01:05:25,045 | executing NPM: ['npm', 'install', '-q', '--no-audit', '--no-save', '--unsafe-perm', '--production']                                                          
2023-12-08 01:05:33,828 | NodejsNpmBuilder:NpmInstall succeeded                                                                                                                        
2023-12-08 01:05:33,829 |  Running NodejsNpmBuilder:CleanUpNpmrc

キャッシュを持っているとnpm installは省略されることがあるようですが一旦別の場所に退避・複製が行われるためその分の時間はかかってしまいます。

2023-12-08 01:56:36,909 | NodejsNpmBuilder:CleanUp succeeded
2023-12-08 01:56:36,909 |  Running NodejsNpmBuilder:CopyDependencies
2023-12-08 01:56:36,910 | Creating target folders at .aws-sam/deps/c39c3d19-50b7-449f-b51f-6b1bdf4c1986/node_modules
2023-12-08 01:56:36,910 | Copying directory metadata from source (/Users/xxxx/sam-app-node/.aws-sam/build/HelloWorldFunction/node_modules) to destination (.aws-sam/deps/c39c3d19-50b7-449f-b51f-6b1bdf4c1986/node_modules)
2023-12-08 01:56:36,911 | Creating target folders at .aws-sam/deps/c39c3d19-50b7-449f-b51f-6b1bdf4c1986/node_modules/proxy-from-env
2023-12-08 01:56:36,911 | Copying directory metadata from source (/Users/xxxx/sam-app-node/.aws-sam/build/HelloWorldFunction/node_modules/proxy-from-env) to destination
(.aws-sam/deps/c39c3d19-50b7-449f-b51f-6b1bdf4c1986/node_modules/proxy-from-env)
2023-12-08 01:56:36,912 | Copying source file (/Users/xxxx/sam-app-node/.aws-sam/build/HelloWorldFunction/node_modules/proxy-from-env/test.js) to destination
(.aws-sam/deps/c39c3d19-50b7-449f-b51f-6b1bdf4c1986/node_modules/proxy-from-env/test.js)
2023-12-08 01:56:36,912 | Copying source file (/Users/xxxx/sam-app-node/.aws-sam/build/HelloWorldFunction/node_modules/proxy-from-env/LICENSE) to destination
(.aws-sam/deps/c39c3d19-50b7-449f-b51f-6b1bdf4c1986/node_modules/proxy-from-env/LICENSE)
2023-12-08 01:56:36,913 | Copying source file (/Users/xxxx/sam-app-node/.aws-sam/build/HelloWorldFunction/node_modules/proxy-from-env/.eslintrc) to destination
(.aws-sam/deps/c39c3d19-50b7-449f-b51f-6b1bdf4c1986/node_modules/proxy-from-env/.eslintrc)

今回のオプションを指定すると

こちらのオプションを指定した場合前述の2.のステップを実行したのち元々のソースディレクトリ内のnode_modulesにシンボリックリンクを貼りnpm updateが実行されます(installではないらしい)。

% sam build --no-cached --debug --build-in-source
...
2023-12-08 00:34:29,953 | Creating target folders at /Users/xxxx/sam-app-node/.aws-sam/build/HelloWorldFunction                                                            
2023-12-08 00:34:29,954 | Copying directory metadata from source (/var/folders/8m/b717gxmx46l7ytmt2smb507w0000gp/T/tmpw2owilpv/unpacked/package) to destination                        
(/Users/xxxx/sam-app-node/.aws-sam/build/HelloWorldFunction)                                                                                                               
2023-12-08 00:34:29,954 | Copying source file (/var/folders/8m/b717gxmx46l7ytmt2smb507w0000gp/T/tmpw2owilpv/unpacked/package/package.json) to destination                              
(/Users/xxxx/sam-app-node/.aws-sam/build/HelloWorldFunction/package.json)                                                                                                  
2023-12-08 00:34:29,955 | Copying source file (/var/folders/8m/b717gxmx46l7ytmt2smb507w0000gp/T/tmpw2owilpv/unpacked/package/app.mjs) to destination                                   
(/Users/xxxx/sam-app-node/.aws-sam/build/HelloWorldFunction/app.mjs)                                                                                                       
2023-12-08 00:34:29,955 | NodejsNpmBuilder:CopySource succeeded                                                                                                                        
2023-12-08 00:34:29,956 |  Running NodejsNpmBuilder:NpmUpdate                                                                                                                          
2023-12-08 00:34:29,956 | NODEJS updating in: /Users/xxxx/sam-app-node/hello-world                                                                                         
2023-12-08 00:34:29,956 | executing NPM: ['npm', 'update', '--no-audit', '--no-save', '--unsafe-perm', '--production', '--no-package-lock', '--install-links']                         
2023-12-08 00:34:36,950 | NodejsNpmBuilder:NpmUpdate succeeded                                                                                                                         
2023-12-08 00:34:36,951 |  Running NodejsNpmBuilder:LinkSource                                                                                                                         
2023-12-08 00:34:36,951 | Creating symlink; source: /Users/xxxx/sam-app-node/hello-world/node_modules, destination:                                                        
/Users/xxxx/sam-app-node/.aws-sam/build/HelloWorldFunction/node_modules                                                                                                    
2023-12-08 00:34:36,952 | NodejsNpmBuilder:LinkSource succeeded                                                                                                                        
2023-12-08 00:34:36,952 |  Running NodejsNpmBuilder:CleanUpNpmrc                                                                                                                       
2023-12-08 00:34:36,952 | NodejsNpmBuilder:CleanUpNpmrc succeeded                                                                                                                      
2023-12-08 00:34:36,953 | Async execution completed                                                                                                                                    
2023-12-08 00:34:36,954 | There is no customer defined id or cdk path defined for resource HelloWorldFunction, so we will use the resource logical id as the resource id               
2023-12-08 00:34:36,954 | 2 resources found in the stack                                                                                                                               
2023-12-08 00:34:36,955 | Found Serverless function with name='HelloWorldFunction' and CodeUri='hello-world/'
2023-12-08 00:34:29,955 | NodejsNpmBuilder:CopySource succeeded                                                                                                                        
2023-12-08 00:34:29,956 |  Running NodejsNpmBuilder:NpmUpdate                                                                                                                          
2023-12-08 00:34:29,956 | NODEJS updating in: /Users/xxxx/sam-app-node/hello-world                                                                                         
2023-12-08 00:34:29,956 | executing NPM: ['npm', 'update', '--no-audit', '--no-save', '--unsafe-perm', '--production', '--no-package-lock', '--install-links']                         
2023-12-08 00:34:36,950 | NodejsNpmBuilder:NpmUpdate succeeded

ビルド結果を確認してみるとnode_modulesに元々のソースコード側を参照するようなシンボリックリンクが貼られていることがわかります。

% tree .aws-sam/build
.aws-sam/build
├── HelloWorldFunction
│   ├── app.mjs
│   ├── node_modules -> /Users/xxxxx/hello-world/node_modules
│   └── package.json
└── template.yaml

インストール先がソースコード格納先となるのでそちらの方にnode_modulesができてインストールされる形となります。

% ls -l hello-world 
total 96
-rw-r--r--   1 xxxxx  staff    761 12  7 19:18 app.mjs
drwxr-xr-x  95 xxxxx  staff   3040 12  8 01:56 node_modules
-rw-r--r--   1 xxxxx  staff  38587 12  8 01:56 package-lock.json
-rw-r--r--   1 xxxxx  staff    469 12  8 01:56 package.json
drwxr-xr-x   3 xxxxx  staff     96 12  7 19:18 tests

終わりに

自分が正直このオプションのメリットを正確に理解できていない気がしますが少し面白いアップデートが入っているような気がします。
確認遅くなってしまうかもしれませんがぜひはてブコメントやTwitter共有コメントで有識者の嬉しい点のご意見いただけると助かります。

キャッシュなしでビルドする際には毎回npm installの回避、キャッシュが存在しても一部ファイルの複製コストはなくせるため少なくともビルドは高速化は見込めますし、
(他に良い方法があった気がしますが)特別事情でnode_modulesの中身を書き換える際に持ち込みが簡単になるのでような使い方はあるのかなと思います。