Pular para o conteúdo

Empacotamento Docker

Vários geradores (como ts#strands-agent e py#strands-agent) produzem uma imagem Docker que é enviada para o Amazon ECR e consumida pela infraestrutura AWS. Este guia descreve o padrão que eles seguem para que você possa aplicá-lo a outros casos de uso — por exemplo, executar um projeto py#fast-api no Amazon ECS, ou implantar um servidor Express containerizado.

O padrão recomendado tem três partes:

  1. Um target bundle no seu projeto que produz um diretório autocontido de artefatos de runtime. Para TypeScript, isso é um bundle JavaScript de arquivo único com tree-shaking produzido pelo Rolldown; para Python, isso é um requirements.txt e dependências instaladas produzidos pelo uv.
  2. Um Dockerfile mínimo que simplesmente faz COPY da saída do bundle para uma imagem base. Como o bundling já lidou com tree-shaking e instalação de dependências, o Dockerfile não precisa executar npm install ou uv sync.
  3. Um target docker que copia o Dockerfile junto com a saída do bundle (para que o contexto de build do Docker contenha apenas arquivos necessários em runtime), e então executa docker build.

O contexto de build do Docker é escrito na pasta dist do seu projeto. Sua infraestrutura como código (CDK ou Terraform) então aponta para esse diretório para enviar a imagem para o ECR.

Configure um target bundle que invoca o Rolldown. Se você está começando de um ts#project, adicione o seguinte ao seu 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 um rolldown.config.ts na raiz do seu projeto:

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

Execute o target bundle para produzir dist/packages/my-project/bundle/index.js:

Terminal window
pnpm nx bundle my-project

Crie um Dockerfile no diretório de origem do seu projeto. O arquivo não faz nada além de COPY do bundle para uma imagem base Node, além de npm install de quaisquer pacotes external que não puderam ser empacotados. Coloque a etapa RUN npm install antes do COPY, para que o Docker possa fazer cache da camada node_modules instalada e só re-executá-la quando a lista de dependências realmente mudar:

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

Adicione um target docker que:

  1. Copia o Dockerfile para o diretório de saída do bundle (para que o contexto de build contenha apenas o bundle + Dockerfile), e
  2. Executa docker build (opcional para CDK — veja abaixo).
{
"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"]
}
}
}

Executar este target produz uma imagem local com tag my-scope-my-project:latest, construída a partir do contexto mínimo em dist/packages/my-project/bundle/:

Terminal window
pnpm nx docker my-project

Configure um target bundle que usa uv para exportar e instalar dependências para sua plataforma alvo. O gerador py#project e o gerador py#lambda-function configuram isso para você. A configuração do target se parece com:

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

Executar nx bundle my-project produz dist/packages/my-project/bundle-arm/ contendo o código-fonte do seu projeto, suas dependências e um requirements.txt — tudo que a imagem precisa em runtime.

O Dockerfile simplesmente copia o bundle para uma imagem base Python. Como o uv já instalou todas as dependências no diretório do bundle, você não precisa executar pip install dentro da imagem:

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

Adicione um target docker que copia o Dockerfile para o diretório de saída do bundle, e então executa 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"]
}
}
}

Isso limpa o diretório de saída, e então copia tanto o conteúdo do bundle quanto o Dockerfile para dist/.../docker, que se torna o contexto de build do Docker.

Terminal window
pnpm nx docker my-project

Conectar o diretório de contexto de build resultante à infraestrutura como código é o mesmo tanto para TypeScript quanto para Python — apenas o caminho para o diretório de contexto de build difere (dist/packages/my-project/bundle para TypeScript, dist/packages/my-project/docker para Python).

Use o DockerImageAsset do CDK apontando para o diretório de contexto de build. O CDK construirá a imagem e a publicará no repositório ECR de assets do CDK no momento do 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,
});

O helper findWorkspaceRoot é gerado pelo gerador ts#infra e exportado de :my-scope/common-constructs. Se você não está usando constructs compartilhados, você pode codificar o caminho para o diretório dist relativo a onde cdk é invocado — tipicamente a raiz do workspace — e omitir completamente a chamada findWorkspaceRoot.

Use o DockerImageAsset com qualquer construct AWS que aceite uma imagem de container, por exemplo aws_ecs.ContainerImage.fromDockerImageAsset(image).