콘텐츠로 이동

Docker 번들링

여러 제너레이터(예: ts#strands-agentpy#strands-agent)는 Amazon ECR로 푸시되고 AWS 인프라에서 사용되는 Docker 이미지를 생성합니다. 이 가이드는 이들이 따르는 패턴을 설명하므로 다른 사용 사례에 적용할 수 있습니다. 예를 들어 Amazon ECS에서 py#fast-api 프로젝트를 실행하거나 컨테이너화된 Express 서버를 배포하는 경우입니다.

권장 패턴은 세 가지 요소로 구성됩니다:

  1. bundle 타겟 - 프로젝트에서 런타임 아티팩트의 독립적인 디렉토리를 생성합니다. TypeScript의 경우 Rolldown에서 생성한 트리 셰이킹된 단일 파일 JavaScript 번들이고, Python의 경우 uv에서 생성한 requirements.txt 및 설치된 종속성입니다.
  2. 최소한의 Dockerfile - 번들 출력을 베이스 이미지로 단순히 COPY합니다. 번들링이 이미 트리 셰이킹과 종속성 설치를 처리했기 때문에 Dockerfilenpm install 또는 uv sync를 실행할 필요가 없습니다.
  3. docker 타겟 - Dockerfile을 번들 출력과 함께 복사하여(Docker 빌드 컨텍스트가 런타임에 필요한 파일만 포함하도록) docker build를 실행합니다.

Docker 빌드 컨텍스트는 프로젝트의 dist 폴더에 작성됩니다. 그런 다음 코드형 인프라(CDK 또는 Terraform)가 해당 디렉토리를 가리켜 이미지를 ECR로 푸시합니다.

Rolldown을 호출하는 bundle 타겟을 구성합니다. ts#project에서 시작하는 경우 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"]
}
}
}

그리고 프로젝트 루트에 rolldown.config.ts를 추가합니다:

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

번들 타겟을 실행하여 dist/packages/my-project/bundle/index.js를 생성합니다:

Terminal window
pnpm nx bundle my-project

프로젝트 소스 디렉토리에 Dockerfile을 생성합니다. 이 파일은 번들을 Node 베이스 이미지로 COPY하고 번들링할 수 없었던 external 패키지를 npm install하는 것 이상의 작업을 수행하지 않습니다. RUN npm install 단계를 COPY 이전에 배치하여 Docker가 설치된 node_modules 레이어를 캐시하고 종속성 목록이 실제로 변경될 때만 다시 실행하도록 합니다:

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

다음을 수행하는 docker 타겟을 추가합니다:

  1. Dockerfile을 번들 출력 디렉토리로 복사하고(빌드 컨텍스트가 번들 + Dockerfile만 포함하도록),
  2. docker build를 실행합니다(CDK의 경우 선택 사항 — 아래 참조).
{
"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"]
}
}
}

이 타겟을 실행하면 dist/packages/my-project/bundle/의 최소 컨텍스트에서 빌드된 my-scope-my-project:latest 태그가 지정된 로컬 이미지가 생성됩니다:

Terminal window
pnpm nx docker my-project

대상 플랫폼에 대한 종속성을 내보내고 설치하기 위해 uv를 사용하는 bundle 타겟을 구성합니다. py#project 제너레이터와 py#lambda-function 제너레이터 모두 이를 구성합니다. 타겟 구성은 다음과 같습니다:

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

nx bundle my-project를 실행하면 프로젝트 소스, 종속성 및 requirements.txt를 포함하는 dist/packages/my-project/bundle-arm/이 생성됩니다 — 이미지가 런타임에 필요한 모든 것입니다.

Dockerfile은 단순히 번들을 Python 베이스 이미지로 복사합니다. uv가 이미 모든 종속성을 번들 디렉토리에 설치했기 때문에 이미지 내부에서 pip install을 실행할 필요가 없습니다:

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

Dockerfile을 번들 출력 디렉토리로 복사한 다음 docker build를 실행하는 docker 타겟을 추가합니다:

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

이것은 출력 디렉토리를 지우고 번들 내용과 Dockerfile을 모두 dist/.../docker로 복사하여 Docker 빌드 컨텍스트가 됩니다.

Terminal window
pnpm nx docker my-project

결과 빌드 컨텍스트 디렉토리를 코드형 인프라에 연결하는 것은 TypeScript와 Python 모두 동일합니다 — 빌드 컨텍스트 디렉토리 경로만 다릅니다(TypeScript의 경우 dist/packages/my-project/bundle, Python의 경우 dist/packages/my-project/docker).

빌드 컨텍스트 디렉토리를 가리키는 CDK의 DockerImageAsset을 사용합니다. CDK는 배포 시 이미지를 빌드하고 CDK 자산 ECR 리포지토리에 게시합니다:

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

findWorkspaceRoot 헬퍼는 ts#infra 제너레이터에 의해 생성되고 :my-scope/common-constructs에서 내보내집니다. 공유 구성을 사용하지 않는 경우 cdk가 호출되는 위치(일반적으로 워크스페이스 루트)를 기준으로 dist 디렉토리 경로를 하드코딩하고 findWorkspaceRoot 호출을 완전히 생략할 수 있습니다.

컨테이너 이미지를 허용하는 모든 AWS 구성과 함께 DockerImageAsset을 사용합니다. 예를 들어 aws_ecs.ContainerImage.fromDockerImageAsset(image).