Empaquetado Docker
Varios generadores (como ts#agent y py#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 FastAPI en Amazon ECS, o desplegar un servidor Express en contenedor.
El Patrón
Sección titulada «El Patrón»El patrón recomendado tiene tres piezas:
- Un target
bundleen 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 unrequirements.txty las dependencias instaladas producidas por uv. - Un
Dockerfilemínimo que simplemente haceCOPYde la salida del bundle en una imagen base. Debido a que el bundling ya manejó el tree-shaking y la instalación de dependencias, elDockerfileno necesita ejecutarnpm installouv sync. - Un target
dockerque copia elDockerfilejunto 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 ejecutadocker 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.
TypeScript
Sección titulada «TypeScript»Bundle Target
Sección titulada «Bundle Target»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:
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:
pnpm nx bundle my-projectyarn nx bundle my-projectnpx nx bundle my-projectbunx nx bundle my-projectDockerfile
Sección titulada «Dockerfile»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 applicationCOPY index.js /app
EXPOSE 8080
CMD ["node", "index.js"]Docker Target
Sección titulada «Docker Target»Agrega un target docker que:
- Copie el
Dockerfileen el directorio de salida del bundle (para que el contexto de construcción contenga solo el bundle +Dockerfile), y - 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/:
pnpm nx docker my-projectyarn nx docker my-projectnpx nx docker my-projectbunx nx docker my-projectBundle Target
Sección titulada «Bundle Target»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.
Dockerfile
Sección titulada «Dockerfile»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.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"]Docker Target
Sección titulada «Docker Target»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.
pnpm nx docker my-projectyarn nx docker my-projectnpx nx docker my-projectbunx nx docker my-projectEscaneo de Imágenes con Trivy
Sección titulada «Escaneo de Imágenes con Trivy»Es una buena práctica escanear tus imágenes en busca de vulnerabilidades conocidas. Los generadores que siguen este patrón agregan un target trivy que escanea la imagen construida con Trivy, ejecutándose desde la imagen Trivy alojada en ECR, y sale con código distinto de cero en hallazgos HIGH o CRITICAL.
Agrega un target trivy que tenga dependsOn en tu target docker. Guarda la imagen construida en un tarball y la escanea a través de un montaje de enlace relativo al workspace, por lo que el mismo comando funciona tanto con docker como con 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"] } }}Los targets de escaneo por imagen de cada proyecto se agregan bajo un target trivy a nivel de workspace, que el script raíz trivy proporcionado ejecuta (nx run-many --target trivy).
pnpm trivyyarn trivynpm run trivybun trivyInfraestructura
Sección titulada «Infraestructura»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).
El proveedor de AWS de Terraform no tiene un recurso de primera clase para “construir y enviar una imagen Docker”. El patrón usado por los generadores es:
- El target
builddel proyecto ejecutadocker build, produciendo una imagen local etiquetada comomy-scope-my-project:latest. - Un
aws_ecr_repositorypara almacenar la imagen. - Un
null_resourcecon un provisionadorlocal-execque autentica en ECR, re-etiqueta la imagen construida localmente con su digest de contenido, y la envía. - El recurso downstream (por ejemplo,
aws_ecs_task_definition) referencia la imagen por su etiqueta inmutable basada en 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 }}El bloque data.external.docker_digest asegura que el null_resource se vuelva a ejecutar cada vez que cambie el hash de la imagen local, desencadenando un nuevo push en cada cambio de código significativo. La imagen se envía bajo una etiqueta inmutable basada en contenido derivada de su digest, por lo que el repositorio ECR puede usar mutabilidad de etiqueta IMMUTABLE y rechazar cualquier intento de sobrescribir una etiqueta existente.
Lectura Adicional
Sección titulada «Lectura Adicional»- Generador
ts#agent— un ejemplo completo de este patrón para un agente TypeScript desplegado en Bedrock AgentCore Runtime. - Generador
py#agent— el equivalente para Python. - Documentación de Rolldown — referencia de configuración para el bundler de TypeScript.
- Documentación de
uv— referencia para exportación e instalación de dependencias de Python.