Skip to content

Docker Bundling

いくつかのジェネレーター(ts#agentpy#agent など)は、Amazon ECR にプッシュされ、AWS インフラストラクチャによって使用される Docker イメージを生成します。このガイドでは、これらが従うパターンについて説明し、他のユースケース(たとえば、Amazon ECS で FastAPI プロジェクトを実行したり、コンテナ化された Express サーバーをデプロイしたりする場合)に適用できるようにします。

Diagram

推奨されるパターンには 3 つの要素があります:

  1. bundle ターゲット - ランタイムアーティファクトの自己完結型ディレクトリを生成するプロジェクトのターゲット。TypeScript の場合、これは Rolldown によって生成されるツリーシェイクされた単一ファイルの JavaScript バンドルです。Python の場合、これは uv によって生成される requirements.txt とインストールされた依存関係です。
  2. 最小限の Dockerfile - バンドル出力をベースイメージに単純に COPY するだけのもの。バンドリングがすでにツリーシェイクと依存関係のインストールを処理しているため、Dockerfilenpm installuv sync を実行する必要がありません。
  3. docker ターゲット - Dockerfile をバンドル出力と一緒にコピーし(Docker ビルドコンテキストにランタイムに必要なファイルのみが含まれるようにします)、その後 docker build を実行します。

Docker ビルドコンテキストは、プロジェクトの dist フォルダーに書き込まれます。その後、インフラストラクチャコード(CDK または Terraform)がそのディレクトリを指して、イメージを ECR にプッシュします。

Rolldown を呼び出す bundle ターゲットを設定します。ts#project から始める場合は、project.json に以下を追加します:

{
"targets": {
"bundle": {
"cache": true,
"executor": "nx:run-commands",
"outputs": ["{workspaceRoot}/dist/{projectRoot}/bundle"],
"options": {
"command": "rolldown -c rolldown.config.ts",
"cwd": "{projectRoot}"
},
"dependsOn": ["compile"]
}
}
}

そして、プロジェクトのルートに rolldown.config.ts を作成します:

rolldown.config.ts
import { defineConfig } from 'rolldown';
export default defineConfig([
{
tsconfig: 'tsconfig.lib.json',
input: 'src/index.ts',
output: {
file: '../../dist/packages/my-project/bundle/index.js',
format: 'cjs',
codeSplitting: false,
},
platform: 'node',
},
]);

バンドルターゲットを実行して dist/packages/my-project/bundle/index.js を生成します:

Terminal window
pnpm nx bundle my-project

プロジェクトのソースディレクトリに Dockerfile を作成します。このファイルは、バンドルを Node ベースイメージに COPY するだけで、バンドルできなかった external パッケージを npm install します。RUN npm install ステップを COPY前に配置して、Docker がインストールされた node_modules レイヤーをキャッシュし、依存関係リストが実際に変更された場合にのみ再実行できるようにします:

FROM public.ecr.aws/docker/library/node:lts
WORKDIR /app
# Install packages that cannot be bundled (declared as "external" in rolldown.config.ts).
# Kept above the COPY so this layer is cached and only invalidated when the install list changes.
RUN npm install @aws/aws-distro-opentelemetry-node-autoinstrumentation@0.10.0
# Copy bundled application
COPY index.js /app
EXPOSE 8080
CMD ["node", "index.js"]

以下を行う docker ターゲットを追加します:

  1. Dockerfile をバンドル出力ディレクトリにコピーします(ビルドコンテキストにバンドル + Dockerfile のみが含まれるようにします)、そして
  2. docker build を実行します(CDK の場合はオプション — 以下を参照)。
{
"targets": {
"docker": {
"cache": true,
"executor": "nx:run-commands",
"options": {
"commands": [
"ncp packages/my-project/src/Dockerfile dist/packages/my-project/bundle/Dockerfile",
"docker build --platform linux/arm64 -t my-scope-my-project:latest dist/packages/my-project/bundle"
],
"parallel": false
},
"dependsOn": ["bundle"]
}
}
}

このターゲットを実行すると、dist/packages/my-project/bundle/ の最小限のコンテキストからビルドされた、my-scope-my-project:latest とタグ付けされたローカルイメージが生成されます:

Terminal window
pnpm nx docker my-project

uv を使用してターゲットプラットフォームの依存関係をエクスポートおよびインストールする bundle ターゲットを設定します。py#project ジェネレーターと py#lambda-function ジェネレーターの両方がこれを設定します。ターゲット設定は次のようになります:

{
"targets": {
"bundle-arm": {
"cache": true,
"executor": "nx:run-commands",
"outputs": ["{workspaceRoot}/dist/{projectRoot}/bundle-arm"],
"options": {
"commands": [
"uv export --frozen --no-dev --no-editable --project {projectRoot} --package my_project -o dist/{projectRoot}/bundle-arm/requirements.txt",
"uv pip install -n --no-deps --no-installer-metadata --no-compile-bytecode --python-platform aarch64-manylinux_2_28 --target dist/{projectRoot}/bundle-arm -r dist/{projectRoot}/bundle-arm/requirements.txt"
],
"parallel": false
},
"dependsOn": ["compile"]
}
}
}

nx bundle my-project を実行すると、プロジェクトのソース、その依存関係、および requirements.txt を含む dist/packages/my-project/bundle-arm/ が生成されます — イメージがランタイムに必要とするすべてのものです。

Dockerfile は、バンドルを Python ベースイメージに単純にコピーします。uv がすでにすべての依存関係をバンドルディレクトリにインストールしているため、イメージ内で pip install を実行する必要はありません:

FROM public.ecr.aws/docker/library/python:3.14-slim
WORKDIR /app
# Copy bundled package (source + installed dependencies)
COPY . /app
EXPOSE 8080
ENV PYTHONPATH=/app
ENV PATH="/app/bin:${PATH}"
CMD ["python", "-m", "my_project.main"]

Dockerfile をバンドル出力ディレクトリにコピーし、その後 docker build を実行する docker ターゲットを追加します:

{
"targets": {
"docker": {
"cache": true,
"executor": "nx:run-commands",
"options": {
"commands": [
"rimraf dist/packages/my-project/docker",
"make-dir dist/packages/my-project/docker",
"ncp dist/packages/my-project/bundle-arm dist/packages/my-project/docker",
"ncp packages/my-project/src/Dockerfile dist/packages/my-project/docker/Dockerfile",
"docker build --platform linux/arm64 -t my-scope-my-project:latest dist/packages/my-project/docker"
],
"parallel": false
},
"dependsOn": ["bundle-arm"]
}
}
}

これにより、出力ディレクトリがクリアされ、バンドルの内容と Dockerfile の両方が dist/.../docker にコピーされ、これが Docker ビルドコンテキストになります。

Terminal window
pnpm nx docker my-project

Trivy でイメージをスキャンする

Section titled “Trivy でイメージをスキャンする”

イメージに既知の脆弱性がないかスキャンすることは良い習慣です。このパターンに従うジェネレーターは、Trivy でビルドされたイメージをスキャンする trivy ターゲットを追加します。これは ECR でホストされている Trivy イメージから実行され、HIGH または CRITICAL の検出結果でゼロ以外の終了コードを返します。

docker ターゲットに dependsOn する trivy ターゲットを追加します。ビルドされたイメージを tarball に保存し、ワークスペース相対のバインドマウントを介してスキャンするため、同じコマンドが dockerfinch の両方で機能します:

{
"targets": {
"trivy": {
"cache": true,
"inputs": ["default", "^production"],
"outputs": ["{workspaceRoot}/dist/{projectRoot}/trivy"],
"executor": "nx:run-commands",
"options": {
"commands": [
"rimraf dist/packages/my-project/trivy",
"make-dir dist/packages/my-project/trivy",
"ncp packages/my-project/.trivyignore dist/packages/my-project/trivy/.trivyignore",
"docker save -o dist/packages/my-project/trivy/image.tar my-scope-my-project:latest",
"docker run --rm -v \"./dist/packages/my-project/trivy\":/scan public.ecr.aws/aquasecurity/trivy:0.72.0 image --input /scan/image.tar --ignorefile /scan/.trivyignore --scanners vuln --severity HIGH,CRITICAL --ignore-unfixed --exit-code 1 --no-progress -q"
],
"parallel": false
},
"dependsOn": ["docker"]
}
}
}

各プロジェクトのイメージごとのスキャンターゲットは、ワークスペース全体の trivy ターゲットの下に集約され、提供される trivy ルートスクリプトが実行します(nx run-many --target trivy)。

Terminal window
pnpm trivy

結果のビルドコンテキストディレクトリをインフラストラクチャコードに接続することは、TypeScript と Python の両方で同じです — ビルドコンテキストディレクトリへのパスのみが異なります(TypeScript の場合は dist/packages/my-project/bundle、Python の場合は dist/packages/my-project/docker)。

ビルドコンテキストディレクトリを指す CDK の DockerImageAsset を使用します。CDK は、デプロイ時にイメージをビルドし、CDK アセット ECR リポジトリに公開します:

Diagram
import { DockerImageAsset, Platform } from 'aws-cdk-lib/aws-ecr-assets';
import { findWorkspaceRoot } from '@my-scope/common-constructs';
import * as path from 'path';
import * as url from 'url';
const image = new DockerImageAsset(this, 'MyImage', {
directory: path.join(
// Resolve from the compiled construct location to the workspace root
findWorkspaceRoot(url.fileURLToPath(new URL(import.meta.url))),
'dist/packages/my-project/bundle',
),
platform: Platform.LINUX_ARM64,
});

findWorkspaceRoot ヘルパーは、ts#infra ジェネレーターによって生成され、@my-scope/common-constructs からエクスポートされます。共有コンストラクトを使用していない場合は、cdk が呼び出される場所(通常はワークスペースルート)からの dist ディレクトリへのパスをハードコードし、findWorkspaceRoot 呼び出しを完全に省略できます。

DockerImageAsset を、コンテナイメージを受け入れる任意の AWS コンストラクトで使用します。たとえば、aws_ecs.ContainerImage.fromDockerImageAsset(image) などです。