Skip to content

Docker バンドリング

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

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

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

Docker buildコンテキストはプロジェクトのdistフォルダーに書き込まれます。その後、Infrastructure as Code(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',
inlineDynamicImports: true,
},
platform: 'node',
},
]);

bundleターゲットを実行して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.12-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 buildコンテキストになります。

Terminal window
pnpm nx docker my-project

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

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

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)などです。