Bundling Docker
Diversi generatori (come ts#strands-agent e py#strands-agent) producono un’immagine Docker che viene caricata su Amazon ECR e utilizzata dall’infrastruttura AWS. Questa guida descrive il pattern che seguono in modo da poterlo applicare ad altri casi d’uso — ad esempio, eseguire un progetto py#fast-api su Amazon ECS o distribuire un server Express containerizzato.
Il Pattern
Sezione intitolata “Il Pattern”Il pattern raccomandato è composto da tre parti:
- Un target
bundlesul tuo progetto che produce una directory autonoma di artefatti di runtime. Per TypeScript si tratta di un bundle JavaScript a file singolo ottimizzato tramite tree-shaking prodotto da Rolldown; per Python si tratta di unrequirements.txte delle 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 necessita 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 di infrastruttura (CDK o Terraform) punta quindi a quella directory per caricare l’immagine su ECR.
TypeScript
Sezione intitolata “TypeScript”Bundle Target
Sezione intitolata “Bundle Target”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, "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', inlineDynamicImports: true, }, 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 eseguire COPY del bundle in un’immagine base Node, più npm install di eventuali pacchetti external che non possono 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
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"]Docker Target
Sezione intitolata “Docker Target”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": 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’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-projectBundle Target
Sezione intitolata “Bundle Target”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 configurano entrambi questo 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 copia semplicemente 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.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"]Docker Target
Sezione intitolata “Docker Target”Aggiungi un target docker che copia il Dockerfile nella directory di output del bundle, quindi esegue 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"] } }}Questo cancella la directory di output, quindi copia sia i contenuti 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-projectInfrastructure
Sezione intitolata “Infrastructure”Collegare la directory del contesto di build risultante all’infrastruttura come codice è lo stesso sia per TypeScript che per Python — cambia solo il percorso alla directory del contesto di build (dist/packages/my-project/bundle per TypeScript, dist/packages/my-project/docker per Python).
Usa DockerImageAsset di CDK puntando 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 costrutti 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 DockerImageAsset con qualsiasi costrutto AWS che accetti un’immagine container, ad esempio aws_ecs.ContainerImage.fromDockerImageAsset(image).
Il provider AWS di Terraform non ha una risorsa di prima classe “build and push a Docker image”. Il pattern usato dai generatori è:
- Un
aws_ecr_repositoryper contenere l’immagine. - Una
null_resourcecon un provisionerlocal-execche si autentica su ECR, ri-tagga l’immagine costruita localmente e la carica. - La risorsa downstream (ad es.
aws_ecs_task_definition) fa riferimento a"${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 }}Il blocco data.external.docker_digest garantisce che la null_resource venga rieseguita ogni volta che l’hash dell’immagine locale cambia, attivando un nuovo push ad ogni modifica significativa del codice.
Ulteriori Letture
Sezione intitolata “Ulteriori Letture”- generatore
ts#strands-agent— un esempio completo di questo pattern per un agente TypeScript distribuito su Bedrock AgentCore Runtime. - generatore
py#strands-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.