Ir al contenido

Empaquetado Docker

Varios generadores (como ts#strands-agent y py#strands-agent) producen una imagen Docker que se envía a Amazon ECR y es consumida por la infraestructura de AWS. Esta guía describe el patrón que siguen para que puedas aplicarlo a otros casos de uso — por ejemplo, ejecutar un proyecto py#fast-api en Amazon ECS, o desplegar un servidor Express en contenedor.

El patrón recomendado tiene tres piezas:

  1. Un target bundle en tu proyecto que produce un directorio autocontenido de artefactos de tiempo de ejecución. Para TypeScript esto es un bundle JavaScript de un solo archivo con tree-shaking producido por Rolldown; para Python esto es un requirements.txt y las dependencias instaladas producidas por uv.
  2. Un Dockerfile mínimo que simplemente hace COPY de la salida del bundle en una imagen base. Debido a que el bundling ya manejó el tree-shaking y la instalación de dependencias, el Dockerfile no necesita ejecutar npm install o uv sync.
  3. Un target docker que copia el Dockerfile junto con la salida del bundle (para que el contexto de construcción de Docker solo contenga archivos necesarios en tiempo de ejecución), y luego ejecuta docker build.

El contexto de construcción de Docker se escribe en la carpeta dist de tu proyecto. Tu infraestructura como código (CDK o Terraform) luego apunta a ese directorio para enviar la imagen a ECR.

Configura un target bundle que invoque Rolldown. Si estás comenzando desde un ts#project, agrega lo siguiente a tu 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"]
}
}
}

Y un rolldown.config.ts en la raíz de tu proyecto:

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

Ejecuta el target bundle para producir dist/packages/my-project/bundle/index.js:

Terminal window
pnpm nx bundle my-project

Crea un Dockerfile en el directorio fuente de tu proyecto. El archivo no hace nada más que hacer COPY del bundle en una imagen base de Node, además de npm install de cualquier paquete external que no pudo ser empaquetado. Coloca el paso RUN npm install antes del COPY, para que Docker pueda cachear la capa de node_modules instalados y solo volver a ejecutarla cuando la lista de dependencias realmente cambie:

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"]

Agrega un target docker que:

  1. Copie el Dockerfile en el directorio de salida del bundle (para que el contexto de construcción contenga solo el bundle + Dockerfile), y
  2. Ejecute docker build (opcional para CDK — ver abajo).
{
"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"]
}
}
}

Ejecutar este target produce una imagen local etiquetada como my-scope-my-project:latest, construida desde el contexto mínimo en dist/packages/my-project/bundle/:

Terminal window
pnpm nx docker my-project

Configura un target bundle que use uv para exportar e instalar dependencias para tu plataforma objetivo. El generador py#project y el generador py#lambda-function configuran esto por ti. La configuración del target se ve así:

{
"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"]
}
}
}

Ejecutar nx bundle my-project produce dist/packages/my-project/bundle-arm/ conteniendo el código fuente de tu proyecto, sus dependencias y un requirements.txt — todo lo que la imagen necesita en tiempo de ejecución.

El Dockerfile simplemente copia el bundle en una imagen base de Python. Debido a que uv ya instaló todas las dependencias en el directorio del bundle, no necesitas ejecutar pip install dentro de la imagen:

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"]

Agrega un target docker que copie el Dockerfile en el directorio de salida del bundle, y luego ejecute 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"]
}
}
}

Esto limpia el directorio de salida, luego copia tanto el contenido del bundle como el Dockerfile en dist/.../docker, que se convierte en el contexto de construcción de Docker.

Terminal window
pnpm nx docker my-project

Conectar el directorio de contexto de construcción resultante a la infraestructura como código es lo mismo tanto para TypeScript como para Python — solo difiere la ruta al directorio de contexto de construcción (dist/packages/my-project/bundle para TypeScript, dist/packages/my-project/docker para Python).

Usa DockerImageAsset de CDK apuntando al directorio de contexto de construcción. CDK construirá la imagen y la publicará en el repositorio ECR de activos de CDK en el momento del despliegue:

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

El helper findWorkspaceRoot es generado por el generador ts#infra y exportado desde :my-scope/common-constructs. Si no estás usando constructos compartidos, puedes codificar la ruta al directorio dist relativa a donde se invoca cdk — típicamente la raíz del workspace — y omitir la llamada a findWorkspaceRoot por completo.

Usa el DockerImageAsset con cualquier constructo de AWS que acepte una imagen de contenedor, por ejemplo aws_ecs.ContainerImage.fromDockerImageAsset(image).