Bundling Docker
Plusieurs générateurs (tels que ts#agent et py#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 FastAPI 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.14-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-projectAnalyse des Images avec Trivy
Section intitulée « Analyse des Images avec Trivy »C’est une bonne pratique d’analyser vos images pour détecter les vulnérabilités connues. Les générateurs qui suivent ce modèle ajoutent une cible trivy qui analyse l’image construite avec Trivy, s’exécutant depuis l’image Trivy hébergée sur ECR, et se termine avec un code de sortie non-zéro sur les résultats HIGH ou CRITICAL.
Ajoutez une cible trivy qui dependsOn votre cible docker. Elle sauvegarde l’image construite dans un tarball et l’analyse via un montage bind relatif au workspace, de sorte que la même commande fonctionne sous docker et finch :
{ "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"] } }}Les cibles d’analyse par image de chaque projet sont agrégées sous une cible trivy à l’échelle du workspace, que le script racine trivy fourni exécute (nx run-many --target trivy).
pnpm trivyyarn trivynpm run trivybun trivyInfrastructure
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 :
- La cible
builddu projet exécutedocker build, produisant une image locale taguéemy-scope-my-project:latest. - 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 avec son digest de contenu, et la pousse. - La ressource en aval (par exemple
aws_ecs_task_definition) référence l’image par son tag immuable basé sur le digest.
resource "aws_ecr_repository" "repo" { name = "my-project-repository" image_tag_mutability = "IMMUTABLE" 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}}')'\"}'"]}
locals { # Content-based, immutable image tag derived from the local image digest image_tag = replace(data.external.docker_digest.result.digest, "sha256:", "")}
resource "null_resource" "docker_publish" { triggers = { docker_digest = data.external.docker_digest.result.digest repository_url = aws_ecr_repository.repo.repository_url image_tag = local.image_tag }
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}:${self.triggers.image_tag} docker push ${self.triggers.repository_url}:${self.triggers.image_tag} 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. L’image est poussée sous un tag immuable basé sur le contenu dérivé de son digest, de sorte que le dépôt ECR peut utiliser la mutabilité de tag IMMUTABLE et rejeter toute tentative d’écrasement d’un tag existant.
Lectures Complémentaires
Section intitulée « Lectures Complémentaires »- Générateur
ts#agent— un exemple complet de ce modèle pour un agent TypeScript déployé sur Bedrock AgentCore Runtime. - Générateur
py#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.es dépendances Python.