Salta ai contenuti

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 raccomandato è composto da tre parti:

  1. Un target bundle sul 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 un requirements.txt e delle dipendenze installate prodotte da uv.
  2. Un Dockerfile minimale che semplicemente esegue COPY dell’output del bundle in un’immagine base. Poiché il bundling ha già gestito il tree-shaking e l’installazione delle dipendenze, il Dockerfile non necessita di eseguire npm install o uv sync.
  3. Un target docker che copia il Dockerfile accanto all’output del bundle (in modo che il contesto di build Docker contenga solo i file necessari al runtime), quindi esegue docker 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.

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:

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

Esegui il target bundle per produrre dist/packages/my-project/bundle/index.js:

Terminal window
pnpm nx bundle my-project

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 application
COPY index.js /app
EXPOSE 8080
CMD ["node", "index.js"]

Aggiungi un target docker che:

  1. Copia il Dockerfile nella directory di output del bundle (in modo che il contesto di build contenga solo il bundle + Dockerfile), e
  2. 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/:

Terminal window
pnpm nx docker my-project

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.

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=/app
ENV PATH="/app/bin:${PATH}"
CMD ["python", "-m", "my_project.main"]

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.

Terminal window
pnpm nx docker my-project

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).