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
Seção intitulada “O Padrão”O padrão recomendado tem três partes:
- Um target
bundleno 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 é umrequirements.txte dependências instaladas produzidos pelo uv. - Um
Dockerfilemínimo que simplesmente fazCOPYda saída do bundle para uma imagem base. Como o bundling já lidou com tree-shaking e instalação de dependências, oDockerfilenão precisa executarnpm installouuv sync. - Um target
dockerque copia oDockerfilejunto com a saída do bundle (para que o contexto de build do Docker contenha apenas arquivos necessários em runtime), e então executadocker 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.
TypeScript
Seção intitulada “TypeScript”Bundle Target
Seção intitulada “Bundle Target”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:
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:
pnpm nx bundle my-projectyarn nx bundle my-projectnpx nx bundle my-projectbunx nx bundle my-projectDockerfile
Seção intitulada “Dockerfile”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 applicationCOPY index.js /app
EXPOSE 8080
CMD ["node", "index.js"]Docker Target
Seção intitulada “Docker Target”Adicione um target docker que:
- Copia o
Dockerfilepara o diretório de saída do bundle (para que o contexto de build contenha apenas o bundle +Dockerfile), e - 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/:
pnpm nx docker my-projectyarn nx docker my-projectnpx nx docker my-projectbunx nx docker my-projectBundle Target
Seção intitulada “Bundle Target”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.
Dockerfile
Seção intitulada “Dockerfile”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=/appENV PATH="/app/bin:${PATH}"
CMD ["python", "-m", "my_project.main"]Docker Target
Seção intitulada “Docker Target”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.
pnpm nx docker my-projectyarn nx docker my-projectnpx nx docker my-projectbunx nx docker my-projectInfrastructure
Seção intitulada “Infrastructure”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).
O provider AWS do Terraform não tem um recurso de primeira classe para “construir e enviar uma imagem Docker”. O padrão usado pelos geradores é:
- Um
aws_ecr_repositorypara armazenar a imagem. - Um
null_resourcecom um provisionerlocal-execque autentica no ECR, re-tageia a imagem construída localmente e a envia. - O recurso downstream (por exemplo,
aws_ecs_task_definition) referencia"${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 }}O bloco data.external.docker_digest garante que o null_resource seja re-executado sempre que o hash da imagem local mudar, disparando um novo push a cada mudança significativa no código.
Leitura Adicional
Seção intitulada “Leitura Adicional”- gerador
ts#strands-agent— um exemplo completo deste padrão para um agente TypeScript implantado no Bedrock AgentCore Runtime. - gerador
py#strands-agent— o equivalente para Python. - Documentação do Rolldown — referência de configuração para o bundler TypeScript.
- Documentação do
uv— referência para exportação e instalação de dependências Python.