Docker Bundling
Diversi generatori (come ts#agent e py#agent) producono un’immagine Docker che viene inviata ad Amazon ECR e consumata dall’infrastruttura AWS. Questa guida descrive il pattern che seguono in modo che tu possa applicarlo ad altri casi d’uso — ad esempio, eseguire un progetto FastAPI su Amazon ECS, o distribuire un server Express containerizzato.
Il Pattern
Sezione intitolata “Il Pattern”Il pattern raccomandato ha tre componenti:
- Un target
bundlesul tuo progetto che produce una directory autonoma di artefatti runtime. Per TypeScript questo è un bundle JavaScript tree-shaken a file singolo prodotto da Rolldown; per Python questo è unrequirements.txte le dipendenze installate prodotte da uv. - Un
Dockerfileminimale che semplicemente esegueCOPYdell’output del bundle in un’immagine base. Poiché il bundling ha già gestito il tree-shaking e l’installazione delle dipendenze, ilDockerfilenon ha bisogno di eseguirenpm installouv sync. - Un target
dockerche copia ilDockerfileaccanto all’output del bundle (in modo che il contesto di build Docker contenga solo i file necessari al runtime), quindi eseguedocker build.
Il contesto di build Docker viene scritto nella cartella dist del tuo progetto. Il tuo codice infrastrutturale (CDK o Terraform) punta quindi a quella directory per inviare l’immagine a ECR.
TypeScript
Sezione intitolata “TypeScript”Target Bundle
Sezione intitolata “Target Bundle”Configura un target bundle che invoca Rolldown. Se stai partendo da un ts#project, aggiungi quanto segue al tuo project.json:
{ "targets": { "bundle": { "cache": true, "inputs": ["default"], "executor": "nx:run-commands", "outputs": ["{workspaceRoot}/dist/{projectRoot}/bundle"], "options": { "command": "rolldown -c rolldown.config.ts", "cwd": "{projectRoot}" }, "dependsOn": ["compile"] } }}E un rolldown.config.ts alla radice del tuo progetto:
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', },]);Esegui il target bundle per produrre dist/packages/my-project/bundle/index.js:
pnpm nx bundle my-projectyarn nx bundle my-projectnpx nx bundle my-projectbunx nx bundle my-projectDockerfile
Sezione intitolata “Dockerfile”Crea un Dockerfile nella directory sorgente del tuo progetto. Il file non fa altro che applicare gli aggiornamenti di sicurezza dell’immagine base, eseguire COPY del bundle in un’immagine base Node, più npm install di eventuali pacchetti external che non potevano essere inclusi nel bundle. Posiziona lo step RUN npm install prima del COPY, in modo che Docker possa cachare il layer node_modules installato e rieseguirlo solo quando l’elenco delle dipendenze cambia effettivamente:
FROM public.ecr.aws/docker/library/node:lts
# Apply the distribution's security updates: base image tags lag behind the# security suite, so their OS packages carry vulnerabilities that already have a# published fix.RUN apt-get update && \ apt-get upgrade -y --no-install-recommends && \ rm -rf /var/lib/apt/lists/*
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"]Target Docker
Sezione intitolata “Target Docker”Aggiungi un target docker che:
- Copia il
Dockerfilenella directory di output del bundle (in modo che il contesto di build contenga solo il bundle +Dockerfile), e - Esegue
docker build(opzionale per CDK — vedi sotto).
{ "targets": { "docker": { "cache": false, "executor": "nx:run-commands", "options": { "commands": [ "shx cp 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’esecuzione di questo target produce un’immagine locale taggata my-scope-my-project:latest, costruita dal contesto minimale in dist/packages/my-project/bundle/:
pnpm nx docker my-projectyarn nx docker my-projectnpx nx docker my-projectbunx nx docker my-projectTarget Bundle
Sezione intitolata “Target Bundle”Configura un target bundle che usa uv per esportare e installare le dipendenze per la tua piattaforma target. Il generatore py#project e il generatore py#lambda-function lo configurano entrambi per te. La configurazione del target appare così:
{ "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’esecuzione di nx bundle my-project produce dist/packages/my-project/bundle-arm/ contenente il sorgente del tuo progetto, le sue dipendenze e un requirements.txt — tutto ciò di cui l’immagine ha bisogno al runtime.
Dockerfile
Sezione intitolata “Dockerfile”Il Dockerfile applica gli aggiornamenti di sicurezza dell’immagine base, quindi copia il bundle in un’immagine base Python. Poiché uv ha già installato tutte le dipendenze nella directory del bundle, non è necessario eseguire pip install all’interno dell’immagine:
FROM public.ecr.aws/docker/library/python:3.14-slim
# Apply the distribution's security updates: base image tags lag behind the# security suite, so their OS packages carry vulnerabilities that already have a# published fix.RUN apt-get update && \ apt-get upgrade -y --no-install-recommends && \ rm -rf /var/lib/apt/lists/*
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"]Target Docker
Sezione intitolata “Target Docker”Aggiungi un target docker che copia il Dockerfile nella directory di output del bundle, quindi esegue docker build:
{ "targets": { "docker": { "cache": false, "executor": "nx:run-commands", "options": { "commands": [ "shx rm -rf dist/packages/my-project/docker", "shx mkdir -p dist/packages/my-project/docker", "shx cp -R dist/packages/my-project/bundle-arm/. dist/packages/my-project/docker", "shx cp 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"] } }}Questo cancella la directory di output, quindi copia sia il contenuto del bundle che il Dockerfile in dist/.../docker, che diventa il contesto di build Docker.
pnpm nx docker my-projectyarn nx docker my-projectnpx nx docker my-projectbunx nx docker my-projectScansione delle Immagini con Trivy
Sezione intitolata “Scansione delle Immagini con Trivy”È buona pratica scansionare le tue immagini per vulnerabilità note. I generatori che seguono questo pattern aggiungono un target trivy che scansiona l’immagine costruita con Trivy, eseguito dall’immagine Trivy ospitata su ECR, ed esce con codice non-zero su risultati HIGH o CRITICAL.
Il target copia un .trivyignore dalla radice del tuo progetto nella directory di scansione e lo passa a Trivy con --ignorefile, quindi crea prima quel file — uno vuoto va bene, ed è dove sopprimerai i risultati in seguito:
touch packages/my-project/.trivyignoreQuindi aggiungi un target trivy che ha dependsOn sul tuo target docker. Salva l’immagine costruita in un tarball e la scansiona tramite un bind mount relativo al workspace, quindi lo stesso comando funziona sia con docker che con finch:
{ "targets": { "trivy": { "cache": false, "outputs": ["{workspaceRoot}/dist/{projectRoot}/trivy"], "executor": "nx:run-commands", "options": { "commands": [ "shx rm -rf dist/packages/my-project/trivy", "shx mkdir -p dist/packages/my-project/trivy", "shx cp 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"] } }}I target di scansione per-immagine di ogni progetto sono aggregati sotto un target trivy a livello di workspace, che lo script root trivy fornito esegue (nx run-many --target trivy).
pnpm trivyyarn trivynpm run trivybun trivyInfrastruttura
Sezione intitolata “Infrastruttura”Collegare la directory del contesto di build risultante al codice infrastrutturale è lo stesso sia per TypeScript che per Python — solo il percorso alla directory del contesto di build differisce (dist/packages/my-project/bundle per TypeScript, dist/packages/my-project/docker per Python).
Usa il DockerImageAsset di CDK puntato alla directory del contesto di build. CDK costruirà l’immagine e la pubblicherà nel repository ECR degli asset CDK al momento del deploy:
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 è generato dal generatore ts#infra ed esportato da @my-scope/common-constructs. Se non stai usando construct condivisi, puoi codificare il percorso alla directory dist relativo a dove viene invocato cdk — tipicamente la radice del workspace — e omettere completamente la chiamata a findWorkspaceRoot.
Usa il DockerImageAsset con qualsiasi construct AWS che accetta un’immagine container, ad esempio aws_ecs.ContainerImage.fromDockerImageAsset(image).
Il provider AWS di Terraform non ha una risorsa di prima classe “costruisci e invia un’immagine Docker”. Il pattern usato dai generatori è:
- Il target
builddel progetto eseguedocker build, producendo un’immagine locale taggatamy-scope-my-project:latest. - Un repository ECR condiviso che usa il modulo
core/asset-ecr - Una
null_resourcecon un provisionerlocal-execche si autentica a ECR, ri-tagga l’immagine costruita localmente con il suo digest del contenuto, e la invia. - La risorsa downstream (es.
aws_ecs_task_definition) fa riferimento all’immagine tramite il suo tag immutabile basato sul digest.
Poiché un repository contiene ogni immagine, ogni tag è namespaced dal proprietario dell’immagine così come dal suo digest, quindi le immagini che condividono il registry non entrano mai in collisione.
module "asset_ecr" { source = "../../common/terraform/src/core/asset-ecr"}
# 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, and # namespaced by owner so images sharing the registry never collide image_tag = "my-project-${replace(data.external.docker_digest.result.digest, "sha256:", "")}"}
resource "null_resource" "docker_publish" { triggers = { docker_digest = data.external.docker_digest.result.digest repository_url = module.asset_ecr.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 }}Il blocco data.external.docker_digest assicura che la null_resource venga rieseguita ogni volta che l’hash dell’immagine locale cambia, attivando un nuovo push ad ogni cambiamento significativo del codice. L’immagine viene inviata sotto un tag immutabile basato sul contenuto derivato dal suo digest, quindi il repository condiviso usa la mutabilità dei tag IMMUTABLE e rifiuta qualsiasi tentativo di sovrascrivere un tag esistente.
Ulteriori Letture
Sezione intitolata “Ulteriori Letture”- Generatore
ts#agent— un esempio completo di questo pattern per un agent TypeScript distribuito su Bedrock AgentCore Runtime. - Generatore
py#agent— l’equivalente per Python. - Documentazione Rolldown — riferimento di configurazione per il bundler TypeScript.
- Documentazione
uv— riferimento per l’esportazione e l’installazione delle dipendenze Python.