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
Section intitulée « Le Modèle »Le modèle recommandé comporte trois éléments :
- Une cible
bundlesur 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’unrequirements.txtet des dépendances installées produites par uv. - Un
Dockerfileminimal qui se contente deCOPYla 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, leDockerfilen’a pas besoin d’exécuternpm installouuv sync. - Une cible
dockerqui copie leDockerfileà 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écutedocker 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.
TypeScript
Section intitulée « TypeScript »Cible Bundle
Section intitulée « Cible Bundle »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 :
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 :
pnpm nx bundle my-projectyarn nx bundle my-projectnpx nx bundle my-projectbunx nx bundle my-projectDockerfile
Section intitulée « Dockerfile »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 applicationCOPY index.js /app
EXPOSE 8080
CMD ["node", "index.js"]Cible Docker
Section intitulée « Cible Docker »Ajoutez une cible docker qui :
- Copie le
Dockerfiledans le répertoire de sortie du bundle (de sorte que le contexte de build contienne uniquement le bundle +Dockerfile), et - 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/ :
pnpm nx docker my-projectyarn nx docker my-projectnpx nx docker my-projectbunx nx docker my-projectCible Bundle
Section intitulée « Cible Bundle »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.
Dockerfile
Section intitulée « Dockerfile »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=/appENV PATH="/app/bin:${PATH}"
CMD ["python", "-m", "my_project.main"]Cible Docker
Section intitulée « Cible Docker »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.
pnpm nx docker my-projectyarn nx docker my-projectnpx nx docker my-projectbunx nx docker my-projectInfrastructure
Section intitulée « Infrastructure »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).
Le provider AWS de Terraform n’a pas de ressource de première classe “build and push a Docker image”. Le modèle utilisé par les générateurs est :
- Un
aws_ecr_repositorypour contenir l’image. - Une
null_resourceavec un provisionneurlocal-execqui s’authentifie auprès d’ECR, re-tague l’image construite localement et la pousse. - La ressource en aval (par exemple
aws_ecs_task_definition) référence"${aws_ecr_repository.repo.repository_url}:latest".
resource "aws_ecr_repository" "repo" { name = "my-project-repository" image_tag_mutability = "MUTABLE" force_delete = true}
# Invalidate the push whenever the locally-built image digest changesdata "external" "docker_digest" { program = ["sh", "-c", "echo '{\"digest\":\"'$(docker inspect my-scope-my-project:latest --format '{{.Id}}')'\"}'"]}
resource "null_resource" "docker_publish" { triggers = { docker_digest = data.external.docker_digest.result.digest repository_url = aws_ecr_repository.repo.repository_url }
provisioner "local-exec" { command = <<-EOT aws ecr get-login-password --region ${data.aws_region.current.id} \ | docker login --username AWS --password-stdin ${self.triggers.repository_url} docker tag my-scope-my-project:latest ${self.triggers.repository_url}:latest docker push ${self.triggers.repository_url}:latest EOT }}Le bloc data.external.docker_digest garantit que la null_resource se réexécute chaque fois que le hash de l’image locale change, déclenchant un nouveau push à chaque changement de code significatif.
Lectures Complémentaires
Section intitulée « Lectures Complémentaires »- Générateur
ts#strands-agent— un exemple complet de ce modèle pour un agent TypeScript déployé sur Bedrock AgentCore Runtime. - Générateur
py#strands-agent— l’équivalent pour Python. - Documentation Rolldown — référence de configuration pour le bundler TypeScript.
- Documentation
uv— référence pour l’export et l’installation des dépendances Python.