•17 min read

Turborepo Remote Caching ở quy mô lớn: Docker Builds được cắt tỉa, GCS Cache & Tăng tốc CI Pipeline

Turborepo Remote Caching ở quy mô lớn: Docker Builds được cắt tỉa, GCS Cache & Tăng tốc CI Pipeline

Turborepo là một hệ thống build hiệu suất cao dành cho các monorepo JavaScript và TypeScript. Giá trị cốt lõi của nó nằm ở khả năng lập lịch tác vụ thông minh, bộ nhớ đệm dựa trên nội dung (content-addressable caching) và bộ nhớ đệm từ xa (remote caching). Hướng dẫn này trình bày chi tiết việc triển khai một thiết lập Turborepo mạnh mẽ, tập trung vào các bản dựng Docker được tối ưu hóa, bộ nhớ đệm từ xa được hỗ trợ bởi GCS và tăng tốc đáng kể pipeline CI.

Audio Briefing
0:00 / 0:00

Hiểu về Cơ chế Cache của Turborepo

Bộ nhớ đệm của Turborepo hoạt động dựa trên nguyên tắc địa chỉ hóa nội dung. Đối với bất kỳ tác vụ nào (ví dụ: build, test, lint), Turborepo sẽ tính toán một mã băm (hash) dựa trên:

  1. Tệp đầu vào: Mã nguồn, tệp cấu hình, v.v., được định nghĩa bởi inputs trong turbo.json.
  2. Dependencies: Các tệp package.json của gói hiện tại và các dependency bắc cầu của nó.
  3. Biến môi trường: Được chỉ định bởi env trong turbo.json.
  4. Cấu hình Turborepo: Bản thân tệp turbo.json.
  5. Lệnh tác vụ: Lệnh chính xác đang được thực thi.

Nếu một tác vụ có mã băm giống hệt đã được thực thi trước đó, Turborepo sẽ truy xuất kết quả được lưu trong bộ nhớ đệm thay vì chạy lại tác vụ. Kết quả này có thể được lưu trữ cục bộ hoặc trong bộ nhớ đệm từ xa.

Advertisement

Tổng quan Kiến trúc

Kiến trúc được tối ưu hóa của chúng tôi tích hợp Turborepo với Docker và Google Cloud Storage (GCS) để có một quy trình build có khả năng mở rộng, hiệu quả:

  1. Turborepo Monorepo: Cơ sở mã tập trung cho nhiều ứng dụng và gói.
  2. turbo prune: Tạo một tập hợp con tối thiểu của monorepo cần thiết để build một ứng dụng cụ thể, tối ưu hóa ngữ cảnh build của Docker.
  3. Multi-stage Docker Builds: Tận dụng đầu ra turbo prune và bộ nhớ đệm lớp của Docker để tạo ra các image gọn nhẹ, có thể tái tạo.
  4. GCS Remote Cache: Một backend bộ nhớ đệm dùng chung, bền vững cho Turborepo, có thể truy cập bởi tất cả các tác nhân CI và nhà phát triển.
  5. GitHub Actions: Điều phối các quy trình build, test và triển khai, sử dụng bộ nhớ đệm từ xa.

Đánh đổi Kiến trúc

Tính năngƯu điểmNhược điểm
turbo pruneNgữ cảnh Docker tối thiểu, build nhanh hơn, image nhỏ hơnTăng độ phức tạp cho Dockerfile, yêu cầu định nghĩa turbo.json inputs cẩn thận
Multi-stage DockerImage cuối cùng nhỏ hơn, cache lớp tốt hơnDockerfile phức tạp hơn, có khả năng phình to dependency trong thời gian build nếu không cẩn thận
GCS Remote CacheCache dùng chung, tính khả dụng cao, khả năng mở rộng, giảm thời gian CIChi phí GCS, độ phức tạp thiết lập ban đầu, độ trễ mạng khi cache miss
GitHub ActionsCI/CD tích hợp, hệ sinh thái tốtKhóa nhà cung cấp (vendor lock-in), có thể bị giới hạn tốc độ (rate limits) trên các repo lớn

Triển khai turbo prune để Tối ưu hóa Docker Builds

turbo prune rất quan trọng đối với các bản dựng Docker. Nó tạo một thư mục mới chỉ chứa các gói và các dependency của chúng cần thiết để build một ứng dụng mục tiêu cụ thể trong monorepo. Điều này làm giảm đáng kể kích thước ngữ cảnh build của Docker, dẫn đến việc build image nhanh hơn và image nhỏ hơn.

Hãy xem xét một cấu trúc monorepo:

/
├── apps/
│   ├── web/
│   └── api/
├── packages/
│   ├── ui/
│   ├── utils/
│   └── db/
├── turbo.json
├── package.json
└── pnpm-lock.yaml

Để build apps/api, chúng ta chỉ cần apps/api, packages/utils và packages/db. turbo prune sẽ cô lập điều này.

Cấu hình turbo.json

Đảm bảo turbo.json của bạn định nghĩa chính xác inputs và outputs cho các tác vụ. Điều này rất quan trọng để băm và cắt tỉa chính xác.

{
  "$schema": "https://turbo.build/schema.json",
  "pipeline": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**", ".next/**", "build/**"],
      "inputs": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.js", "src/**/*.jsx", "tsconfig.json", "package.json"]
    },
    "test": {
      "dependsOn": ["^build"],
      "outputs": [],
      "inputs": ["src/**/*.test.ts", "src/**/*.test.tsx", "src/**/*.spec.ts", "src/**/*.spec.tsx"]
    },
    "lint": {
      "outputs": []
    },
    "dev": {
      "cache": false,
      "persistent": true
    }
  }
}

Dockerfile với turbo prune

Đây là một Dockerfile đa giai đoạn cho một ứng dụng api, tận dụng turbo prune.

# Stage 1: Build dependencies and application
FROM node:20-alpine AS builder

# Install pnpm globally
RUN corepack enable && corepack prepare pnpm@latest --activate

# Set working directory
WORKDIR /app

# Copy lockfile and package.json files first to leverage Docker cache
# This layer changes infrequently, maximizing cache hits
COPY pnpm-lock.yaml ./
COPY package.json ./
COPY turbo.json ./

# Copy only the pruned monorepo subset for the 'api' application
# This command is executed by the CI/CD pipeline or locally before `docker build`
# Example: `turbo prune --scope=api --docker`
# The output of `turbo prune` is expected in the './.pruned-build' directory
COPY .pruned-build/pnpm-workspace.yaml ./.pruned-build/
COPY .pruned-build/full/ ./.pruned-build/full/
COPY .pruned-build/json/ ./.pruned-build/json/

# Install dependencies using pnpm
# Use --frozen-lockfile to ensure reproducible builds
RUN pnpm install --frozen-lockfile --prod=false

# Copy the actual source code for the pruned packages
# This is crucial: `turbo prune` only copies package.json and lockfiles,
# not the source code itself. We copy it from the original context.
# The `full` directory contains symlinks to the original source.
# We need to copy the actual files.
# This assumes the Docker build context is the root of the monorepo.
COPY apps/api ./apps/api
COPY packages/db ./packages/db
COPY packages/utils ./packages/utils

# Build the 'api' application
# Turborepo will use its cache or build if necessary
RUN pnpm turbo run build --filter=api...

# Stage 2: Production image
FROM node:20-alpine AS runner

# Install pnpm globally (for production dependencies if needed, though often not)
RUN corepack enable && corepack prepare pnpm@latest --activate

WORKDIR /app

# Copy only production dependencies from the builder stage
# This ensures a minimal production image
COPY --from=builder /app/pnpm-lock.yaml ./
COPY --from=builder /app/package.json ./
COPY --from=builder /app/turbo.json ./
COPY --from=builder /app/.pruned-build/pnpm-workspace.yaml ./.pruned-build/
COPY --from=builder /app/.pruned-build/json/ ./.pruned-build/json/

# Install production dependencies
RUN pnpm install --prod --frozen-lockfile

# Copy the built application artifacts from the builder stage
# This includes `dist` directories for the API and its dependencies
COPY --from=builder /app/apps/api/dist ./apps/api/dist
COPY --from=builder /app/packages/db/dist ./packages/db/dist
COPY --from=builder /app/packages/utils/dist ./packages/utils/dist

# Expose port (if applicable)
EXPOSE 3000

# Define the command to run the application
CMD ["node", "apps/api/dist/index.js"]

Giải thích về turbo prune trong ngữ cảnh Docker:

  1. turbo prune --scope=api --docker: Lệnh này, được thực thi trước docker build, tạo một thư mục .pruned-build tại thư mục gốc của monorepo.
    • pnpm-workspace.yaml: Được sao chép vào .pruned-build/pnpm-workspace.yaml.
    • full/: Chứa các symlink đến các tệp package.json của tất cả các gói liên quan.
    • json/: Chứa các tệp package.json của tất cả các gói liên quan, nhưng được làm phẳng.
  2. COPY .pruned-build/...: Chúng ta sao chép các tệp được tạo này vào image Docker.
  3. pnpm install: Với các tệp package.json đã được cắt tỉa và pnpm-lock.yaml, pnpm chỉ cài đặt các dependency cần thiết.
  4. COPY apps/api ./apps/api: Quan trọng là, turbo prune không sao chép các tệp nguồn. Bạn phải sao chép rõ ràng mã nguồn cho ứng dụng mục tiêu và các dependency trực tiếp của nó từ ngữ cảnh monorepo gốc. Đây là lý do tại sao lệnh docker build nên được chạy từ thư mục gốc của monorepo.
  5. pnpm turbo run build --filter=api...: Turborepo build ứng dụng api và các dependency thượng nguồn của nó.

Remote Cache Tự lưu trữ với GCS

Turborepo hỗ trợ nhiều backend remote cache khác nhau. Đối với môi trường doanh nghiệp, một giải pháp tự lưu trữ sử dụng lưu trữ đám mây (GCS, S3) mang lại khả năng kiểm soát, khả năng mở rộng và hiệu quả chi phí.

Thiết lập GCS Bucket

  1. Tạo một GCS bucket: gs://your-turborepo-cache-bucket
  2. Đảm bảo các quyền IAM phù hợp: Tài khoản dịch vụ hoặc người dùng truy cập cache cần các vai trò Storage Object Admin hoặc Storage Object Creator và Storage Object Viewer.

Cấu hình Turborepo cho GCS

Turborepo sử dụng các biến môi trường để cấu hình remote caching.

# .env.local or CI environment variables
TURBO_REMOTE_CACHE_SIGNATURE_KEY="your_strong_secret_key_for_signing_cache_requests"
TURBO_REMOTE_CACHE_READ_ONLY="false" # Set to true for read-only environments
TURBO_REMOTE_CACHE_GCS_BUCKET="your-turborepo-cache-bucket"
TURBO_REMOTE_CACHE_GCS_SERVICE_ACCOUNT_KEY="path/to/your/gcs-service-account-key.json" # Or base64 encoded JSON

Lưu ý bảo mật quan trọng: Không bao giờ commit TURBO_REMOTE_CACHE_SIGNATURE_KEY hoặc TURBO_REMOTE_CACHE_GCS_SERVICE_ACCOUNT_KEY trực tiếp vào kho lưu trữ của bạn. Hãy sử dụng các biến môi trường, hệ thống quản lý bí mật (ví dụ: GitHub Secrets, GCP Secret Manager).

Đối với CI/CD, bạn thường sẽ mã hóa base64 khóa tài khoản dịch vụ JSON và lưu trữ nó dưới dạng bí mật.

# Example for GitHub Actions
echo "${{ secrets.GCP_SERVICE_ACCOUNT_KEY_BASE64 }}" | base64 --decode > gcs-key.json
export TURBO_REMOTE_CACHE_GCS_SERVICE_ACCOUNT_KEY=$(pwd)/gcs-key.json
Advertisement

Tăng tốc CI Pipeline với GitHub Actions

Việc tích hợp các thành phần trên vào một quy trình GitHub Actions giúp giảm đáng kể thời gian build CI.

name: CI Build & Test

on:
  push:
    branches:
      - main
  pull_request:
    branches:
      - main

jobs:
  build-api:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
        with:
          fetch-depth: 0 # Required for Turborepo to correctly detect changed files

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'pnpm' # Cache pnpm dependencies

      - name: Install pnpm
        run: corepack enable && corepack prepare pnpm@latest --activate

      - name: Install monorepo dependencies
        run: pnpm install --frozen-lockfile

      - name: Configure Turborepo Remote Cache (GCS)
        env:
          TURBO_REMOTE_CACHE_SIGNATURE_KEY: ${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }}
          TURBO_REMOTE_CACHE_GCS_BUCKET: your-turborepo-cache-bucket
          GCP_SERVICE_ACCOUNT_KEY_BASE64: ${{ secrets.GCP_SERVICE_ACCOUNT_KEY_BASE64 }}
        run: |
          echo "${{ env.GCP_SERVICE_ACCOUNT_KEY_BASE64 }}" | base64 --decode > gcs-key.json
          export TURBO_REMOTE_CACHE_GCS_SERVICE_ACCOUNT_KEY=$(pwd)/gcs-key.json
          echo "TURBO_REMOTE_CACHE_GCS_SERVICE_ACCOUNT_KEY=$(pwd)/gcs-key.json" >> $GITHUB_ENV
          echo "TURBO_REMOTE_CACHE_SIGNATURE_KEY=${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }}" >> $GITHUB_ENV
          echo "TURBO_REMOTE_CACHE_GCS_BUCKET=your-turborepo-cache-bucket" >> $GITHUB_ENV

      - name: Build API application with Turborepo
        run: pnpm turbo run build --filter=api...

      - name: Run API tests with Turborepo
        run: pnpm turbo run test --filter=api...

      - name: Lint API application with Turborepo
        run: pnpm turbo run lint --filter=api...

      - name: Prune API for Docker build
        run: pnpm turbo prune --scope=api --docker

      - name: Build Docker image for API
        run: docker build -t your-registry/api:${{ github.sha }} -f apps/api/Dockerfile .

      - name: Push Docker image (example)
        # Add Docker login steps here if pushing to a private registry
        # run: docker push your-registry/api:${{ github.sha }}
        run: echo "Docker image built: your-registry/api:${{ github.sha }}"

Các tối ưu hóa chính trong quy trình CI:

  • actions/checkout@v4 với fetch-depth: 0: Đảm bảo Turborepo có quyền truy cập vào toàn bộ lịch sử Git để phát hiện thay đổi và lưu vào bộ nhớ đệm chính xác.
  • actions/setup-node@v4 với cache: 'pnpm': Lưu vào bộ nhớ đệm node_modules cho pnpm, giảm thời gian pnpm install trong các lần chạy tiếp theo.
  • Cấu hình Turborepo Remote Cache: Các biến môi trường được thiết lập để trỏ Turborepo đến bộ nhớ đệm GCS.
  • pnpm turbo run build --filter=api...: Turborepo xây dựng một cách thông minh chỉ ứng dụng api và các dependency của nó. Nếu có cache hit, bước này hoàn thành gần như ngay lập tức.
  • pnpm turbo prune --scope=api --docker: Tạo ngữ cảnh build được tối ưu hóa cho Docker.
  • docker build ... -f apps/api/Dockerfile .: Build image Docker. Vì turbo prune đã giảm ngữ cảnh và Dockerfile là đa giai đoạn, bước này nhanh hơn đáng kể.

Các vấn đề và Khắc phục sự cố trong Sản xuất

  1. Cache Misses Mặc dù Không có Thay đổi Mã:

    • Triệu chứng: Turborepo báo cáo cache MISS ngay cả khi không có mã liên quan nào thay đổi.
    • Nguyên nhân:
      • Sai inputs hoặc outputs trong turbo.json. Nếu một tệp ảnh hưởng đến bản dựng không được liệt kê trong inputs, các thay đổi của nó sẽ không làm mất hiệu lực bộ nhớ đệm. Nếu outputs không được định nghĩa chính xác, Turborepo có thể không lưu trữ toàn bộ đầu ra.
      • Các biến môi trường không được đặt nhất quán. Nếu các biến env được định nghĩa trong turbo.json thay đổi giữa các lần chạy, mã băm sẽ thay đổi.
      • Thay đổi package.json hoặc pnpm-lock.yaml trong một dependency.
      • fetch-depth trong actions/checkout quá nông, ngăn Turborepo phát hiện thay đổi chính xác.
    • Khắc phục:
      • Xem xét kỹ turbo.json inputs và outputs. Sử dụng git diff --name-only <commit-ish> để xác định các tệp đã thay đổi và đảm bảo chúng được bao phủ.
      • Đảm bảo tất cả các biến môi trường liên quan được truyền nhất quán cho Turborepo.
      • Đặt fetch-depth: 0 trong actions/checkout.
      • Chạy pnpm turbo run <task> --dry-run=json để kiểm tra mã băm và đầu vào được tính toán.
  2. turbo prune Không Bao gồm Các Tệp Cần Thiết:

    • Triệu chứng: Docker build thất bại vì một tệp hoặc gói được ứng dụng mục tiêu mong đợi bị thiếu sau turbo prune.
    • Nguyên nhân:
      • Tham số turbo prune của lệnh --scope quá hẹp, hoặc biểu đồ dependency không được Turborepo hiểu đúng.
      • Các lệnh COPY trong Dockerfile không đầy đủ, không sao chép tất cả các tệp nguồn cần thiết từ ngữ cảnh monorepo gốc.
    • Khắc phục:
      • Xác minh biểu đồ dependency bằng cách sử dụng pnpm turbo graph --filter=api.... Đảm bảo tất cả các gói cần thiết được liệt kê.
      • Kiểm tra thủ công thư mục .pruned-build sau khi chạy turbo prune.
      • Kiểm tra lại các lệnh COPY trong Dockerfile của bạn, đảm bảo tất cả các thư mục nguồn cho ứng dụng mục tiêu và các dependency trực tiếp của nó được bao gồm. Hãy nhớ turbo prune chỉ xử lý package.json và lockfile, không phải mã nguồn.
  3. Lỗi Quyền GCS:

    • Triệu chứng: Turborepo không thể tải lên hoặc tải xuống từ GCS với lỗi từ chối quyền.
    • Nguyên nhân: Khóa tài khoản dịch vụ được sử dụng không có đủ quyền IAM trên GCS bucket.
    • Khắc phục: Cấp các vai trò Storage Object Admin hoặc ít nhất Storage Object Creator và Storage Object Viewer cho tài khoản dịch vụ trên GCS bucket cụ thể. Đảm bảo đường dẫn TURBO_REMOTE_CACHE_GCS_SERVICE_ACCOUNT_KEY chính xác và tệp có thể đọc được.
  4. pnpm install Chậm trong Docker:

    • Triệu chứng: Ngay cả với turbo prune, pnpm install mất nhiều thời gian trong Docker build.
    • Nguyên nhân:
      • Các tệp pnpm-lock.yaml hoặc package.json thường xuyên thay đổi, làm mất hiệu lực bộ nhớ đệm lớp Docker cho pnpm install.
      • Không có bộ nhớ đệm pnpm được sử dụng trong Dockerfile.
    • Khắc phục:
      • Đảm bảo pnpm-lock.yaml được commit và cập nhật.
      • Cấu trúc Dockerfile của bạn để sao chép pnpm-lock.yaml, package.json và turbo.json trước, sau đó chạy pnpm install. Điều này tối đa hóa việc lưu vào bộ nhớ đệm lớp.
      • Cân nhắc sử dụng một volume bộ nhớ đệm pnpm dùng chung cho phát triển cục bộ, mặc dù điều này khó hơn trong CI. Bộ nhớ đệm actions/setup-node giúp ích trong CI.

Các câu hỏi thường gặp

Q1: Turborepo xử lý các thay đổi trong các dependency bắc cầu để lưu vào bộ nhớ đệm như thế nào?

Cơ chế băm của Turborepo xem xét package.json của gói hiện tại và tất cả các dependency bắc cầu của nó. Nếu bất kỳ package.json nào trong chuỗi dependency thay đổi, hoặc nếu pnpm-lock.yaml (hoặc yarn.lock, package-lock.json) thay đổi, mã băm cho các tác vụ phụ thuộc vào các gói đó sẽ bị vô hiệu hóa, dẫn đến cache miss. Điều này đảm bảo tính đúng đắn.

Q2: Tôi có thể sử dụng một backend remote cache khác ngoài GCS không?

Có. Turborepo hỗ trợ nhiều nhà cung cấp remote cache khác nhau. Ngoài GCS, nó hỗ trợ nguyên bản Vercel Remote Cache (cho các triển khai Vercel), AWS S3 và một endpoint HTTP chung. Đối với S3, bạn sẽ sử dụng TURBO_REMOTE_CACHE_S3_BUCKET, TURBO_REMOTE_CACHE_S3_REGION và thông tin xác thực AWS. Đối với một endpoint HTTP tùy chỉnh, bạn sẽ cấu hình TURBO_REMOTE_CACHE_URL.

Q3: Tác động của fetch-depth: 0 đến hiệu suất và bảo mật CI là gì?

fetch-depth: 0 hướng dẫn Git tìm nạp toàn bộ lịch sử của kho lưu trữ, không chỉ commit mới nhất.

  • Hiệu suất: Đối với các kho lưu trữ rất lớn với lịch sử dài, điều này có thể thêm vài giây vào thời gian checkout. Tuy nhiên, nó thường không đáng kể so với thời gian tiết kiệm được nhờ bộ nhớ đệm Turborepo hiệu quả.
  • Bảo mật: Việc tìm nạp toàn bộ lịch sử có nghĩa là nhiều dữ liệu hơn được kéo vào CI runner. Đối với các kho lưu trữ công khai, điều này thường không phải là mối lo ngại. Đối với các kho lưu trữ riêng tư, hãy đảm bảo môi trường CI của bạn an toàn. Đó là một sự đánh đổi cần thiết cho việc phát hiện thay đổi chính xác của Turborepo.

Q4: Làm cách nào để gỡ lỗi các vấn đề về cache của Turborepo cục bộ?

Sử dụng cờ --dry-run với pnpm turbo run.

  • pnpm turbo run build --filter=api... --dry-run: Hiển thị các tác vụ nào sẽ chạy và tại sao (ví dụ: cache MISS, cache HIT).
  • pnpm turbo run build --filter=api... --dry-run=json: Cung cấp đầu ra JSON chi tiết, bao gồm mã băm được tính toán cho mỗi tác vụ, đầu vào và đầu ra của nó. Điều này rất có giá trị để hiểu tại sao cache miss xảy ra.
  • Đặt TURBO_LOG_LEVEL=debug để ghi nhật ký chi tiết.

Q5: Image Docker của tôi vẫn lớn mặc dù đã sử dụng turbo prune và multi-stage builds. Có thể có vấn đề gì?

  • Các tệp không cần thiết được sao chép: Kiểm tra lại tệp .dockerignore của bạn. Đảm bảo nó loại trừ các tệp chỉ dành cho phát triển, .git, node_modules (ngoại trừ bước pnpm install) và các tạo phẩm build khác.
  • Các dependency thời gian build trong image runtime: Đảm bảo giai đoạn sản xuất của bạn (runner trong ví dụ) chỉ sao chép các tạo phẩm đã build và các dependency sản xuất. Nếu bạn sao chép node_modules từ giai đoạn builder trực tiếp mà không cài đặt lại bằng --prod, bạn sẽ bao gồm các dependency phát triển.
  • Image cơ sở lớn: node:20-alpine thường gọn nhẹ. Tránh các image cơ sở lớn hơn như node:20 (dựa trên Debian) cho sản xuất.
  • Các công cụ build còn sót lại: Đảm bảo giai đoạn runner của bạn không chứa các trình biên dịch, linter hoặc các công cụ build khác từ giai đoạn builder. Dockerfile ví dụ giải quyết đúng vấn đề này bằng cách chỉ sao chép các thư mục dist cụ thể và cài đặt lại pnpm cho các dependency sản xuất.

Bằng cách áp dụng tỉ mỉ các chiến lược này, các tổ chức có thể giảm đáng kể thời gian build CI, dẫn đến vòng phản hồi nhanh hơn, triển khai thường xuyên hơn và cải thiện năng suất của nhà phát triển.

Share this article:

Stay Updated

Get the latest posts delivered straight to your inbox.

Free Developer Utilities

Free In-Browser Developer Tools

Clean AI CLI logs, build cron expressions, decode JWTs, and calculate chmod permissions offline.

Explore Tools
Advertisement