Aller au contenu

Bundling Docker

Plusieurs générateurs (tels que ts#strands-agent et py#strands-agent) produisent une image Docker qui est poussée vers Amazon ECR et consommée par l’infrastructure AWS. Ce guide décrit le modèle qu’ils suivent afin que vous puissiez l’appliquer à d’autres cas d’usage — par exemple, exécuter un projet py#fast-api sur Amazon ECS, ou déployer un serveur Express conteneurisé.

Le modèle recommandé comporte trois éléments :

  1. Une cible bundle sur votre projet qui produit un répertoire autonome d’artefacts d’exécution. Pour TypeScript, il s’agit d’un bundle JavaScript tree-shaken en un seul fichier produit par Rolldown ; pour Python, il s’agit d’un requirements.txt et des dépendances installées produites par uv.
  2. Un Dockerfile minimal qui se contente de COPY la sortie du bundle dans une image de base. Comme le bundling a déjà géré le tree-shaking et l’installation des dépendances, le Dockerfile n’a pas besoin d’exécuter npm install ou uv sync.
  3. Une cible docker qui copie le Dockerfile à côté de la sortie du bundle (de sorte que le contexte de build Docker contienne uniquement les fichiers nécessaires à l’exécution), puis exécute docker build.

Le contexte de build Docker est écrit dans le dossier dist de votre projet. Votre infrastructure en tant que code (CDK ou Terraform) pointe ensuite vers ce répertoire pour pousser l’image vers ECR.

Configurez une cible bundle qui invoque Rolldown. Si vous partez d’un ts#project, ajoutez ce qui suit à votre 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"]
}
}
}

Et un rolldown.config.ts à la racine de votre projet :

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',
},
]);

Exécutez la cible bundle pour produire dist/packages/my-project/bundle/index.js :

Terminal window
pnpm nx bundle my-project

Créez un Dockerfile dans le répertoire source de votre projet. Le fichier ne fait rien de plus que COPY le bundle dans une image de base Node, plus npm install pour tous les packages external qui n’ont pas pu être bundlés. Placez l’étape RUN npm install avant le COPY, afin que Docker puisse mettre en cache la couche node_modules installée et ne la réexécuter que lorsque la liste des dépendances change réellement :

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"]

Ajoutez une cible docker qui :

  1. Copie le Dockerfile dans le répertoire de sortie du bundle (de sorte que le contexte de build contienne uniquement le bundle + Dockerfile), et
  2. Exécute docker build (optionnel pour CDK — voir ci-dessous).
{
"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"]
}
}
}

L’exécution de cette cible produit une image locale taguée my-scope-my-project:latest, construite à partir du contexte minimal à dist/packages/my-project/bundle/ :

Terminal window
pnpm nx docker my-project

Configurez une cible bundle qui utilise uv pour exporter et installer les dépendances pour votre plateforme cible. Le générateur py#project et le générateur py#lambda-function configurent tous deux cela pour vous. La configuration de la cible ressemble à :

{
"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"]
}
}
}

L’exécution de nx bundle my-project produit dist/packages/my-project/bundle-arm/ contenant la source de votre projet, ses dépendances et un requirements.txt — tout ce dont l’image a besoin à l’exécution.

Le Dockerfile copie simplement le bundle dans une image de base Python. Comme uv a déjà installé toutes les dépendances dans le répertoire du bundle, vous n’avez pas besoin d’exécuter pip install à l’intérieur de l’image :

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"]

Ajoutez une cible docker qui copie le Dockerfile dans le répertoire de sortie du bundle, puis exécute docker build :

{
"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"]
}
}
}

Cela efface le répertoire de sortie, puis copie à la fois le contenu du bundle et le Dockerfile dans dist/.../docker, qui devient le contexte de build Docker.

Terminal window
pnpm nx docker my-project

Connecter le répertoire de contexte de build résultant à l’infrastructure en tant que code est identique pour TypeScript et Python — seul le chemin vers le répertoire de contexte de build diffère (dist/packages/my-project/bundle pour TypeScript, dist/packages/my-project/docker pour Python).

Utilisez le DockerImageAsset de CDK pointant vers le répertoire de contexte de build. CDK construira l’image et la publiera dans le dépôt ECR des assets CDK au moment du déploiement :

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,
});

L’helper findWorkspaceRoot est généré par le générateur ts#infra et exporté depuis :my-scope/common-constructs. Si vous n’utilisez pas de constructs partagés, vous pouvez coder en dur le chemin vers le répertoire dist relatif à l’endroit d’où cdk est invoqué — typiquement la racine du workspace — et omettre complètement l’appel à findWorkspaceRoot.

Utilisez le DockerImageAsset avec n’importe quel construct AWS qui accepte une image de conteneur, par exemple aws_ecs.ContainerImage.fromDockerImageAsset(image).