Empacotamento Docker
Vários geradores (como ts#agent e py#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 FastAPI 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.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
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-projectEscaneando Imagens com Trivy
Seção intitulada “Escaneando Imagens com Trivy”É uma boa prática escanear suas imagens em busca de vulnerabilidades conhecidas. Os geradores que seguem este padrão adicionam um target trivy que escaneia a imagem construída com Trivy, executando a partir da imagem Trivy hospedada no ECR, e sai com código diferente de zero em descobertas HIGH ou CRITICAL.
Adicione um target trivy que dependsOn do seu target docker. Ele salva a imagem construída em um tarball e a escaneia através de um bind mount relativo ao workspace, para que o mesmo comando funcione tanto com docker quanto com 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"] } }}Os targets de escaneamento por imagem de cada projeto são agregados sob um target trivy em todo o workspace, que o script raiz trivy fornecido executa (nx run-many --target trivy).
pnpm trivyyarn trivynpm run trivybun trivyInfrastructure
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 é:
- O target
builddo projeto executadocker build, produzindo uma imagem local com tagmy-scope-my-project:latest. - Um
aws_ecr_repositorypara armazenar a imagem. - Um
null_resourcecom um provisionerlocal-execque autentica no ECR, re-tageia a imagem construída localmente com seu digest de conteúdo e a envia. - O recurso downstream (por exemplo,
aws_ecs_task_definition) referencia a imagem por sua tag imutável baseada em 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 }}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. A imagem é enviada sob uma tag imutável baseada em conteúdo derivada de seu digest, para que o repositório ECR possa usar mutabilidade de tag IMMUTABLE e rejeitar qualquer tentativa de sobrescrever uma tag existente.
Leitura Adicional
Seção intitulada “Leitura Adicional”- gerador
ts#agent— um exemplo completo deste padrão para um agente TypeScript implantado no Bedrock AgentCore Runtime. - gerador
py#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.