Skip to content

buildspec.yml anatomy — every block in a production CodeBuild file

A real production buildspec.yml dissected — phases, artifacts, reports, cache, env vars from Parameter Store and Secrets Manager. Every block explained.

A new engineer joins the team and asks why the buildspec.yml for the main Node app has 80 lines, three phases, two reports blocks, a cache section, and four entries under secrets-manager. The honest answer is that every line is there because something broke at 02:00 on a Tuesday and the file grew a scar. A production buildspec is not the documentation example — it is a layered artifact. This article walks the file block by block.

What’s happening

buildspec.yml is the contract between CodeBuild and your repository. CodeBuild reads it at the start of every build, resolves environment variables, runs the phases in order, and uploads whatever the file declares as artifacts and reports. The phases are the workflow:

  • install — runtime setup. Node version, package manager, system packages.
  • pre_build — anything that runs before the actual build. Linting, type checks, secret resolution, ECR login.
  • build — the build itself. npm run build, docker build, mvn package.
  • post_build — what happens after a successful build. Push the image, upload the bundle, post a status to Slack.

Each phase has an on-failure setting — ABORT (default, stop the build) or CONTINUE (move to the next phase even if this one failed). post_build runs even if build fails when on-failure: CONTINUE is set on build, which is how you upload test reports from a failed build.

The other top-level blocks are the contract on what CodeBuild produces:

  • artifacts — files uploaded to S3 (or passed to the next stage in CodePipeline).
  • reports — JUnit/Cucumber/Cobertura test results CodeBuild parses and surfaces in the report group UI.
  • cache — paths preserved between builds when the project has caching enabled.
  • env — variables resolved at the start of the build, including from Parameter Store and Secrets Manager.

How to verify

The whole anatomy in one file, for a typical Node 20 app that builds, tests, and ships a Docker image to ECR:

version: 0.2

env:
  variables:
    NODE_ENV: production
    AWS_REGION: us-east-1
  parameter-store:
    GITHUB_NPM_TOKEN: /shared/github/npm-read-token
    SENTRY_AUTH_TOKEN: /apps/web/sentry-auth-token
  secrets-manager:
    DATABASE_URL: prod/web/db-url:url
    STRIPE_API_KEY: prod/web/stripe:secret_key
  exported-variables:
    - IMAGE_TAG
    - COMMIT_SHA

phases:
  install:
    runtime-versions:
      nodejs: 20
    commands:
      - corepack enable
      - corepack prepare pnpm@9.0.0 --activate
      - echo "//npm.pkg.github.com/:_authToken=${GITHUB_NPM_TOKEN}" > ~/.npmrc

  pre_build:
    commands:
      - export COMMIT_SHA=${CODEBUILD_RESOLVED_SOURCE_VERSION:0:7}
      - export IMAGE_TAG=${COMMIT_SHA}-$(date +%s)
      - aws ecr get-login-password --region $AWS_REGION
          | docker login --username AWS --password-stdin
            $ECR_REGISTRY
      - pnpm install --frozen-lockfile
      - pnpm run lint
      - pnpm run typecheck

  build:
    on-failure: CONTINUE
    commands:
      - pnpm run test:ci -- --reporter=junit --outputFile=reports/junit.xml
      - pnpm run build
      - docker build
          --build-arg SENTRY_AUTH_TOKEN=$SENTRY_AUTH_TOKEN
          --build-arg COMMIT_SHA=$COMMIT_SHA
          -t $ECR_REGISTRY/web:$IMAGE_TAG
          -t $ECR_REGISTRY/web:latest
          .

  post_build:
    commands:
      - |
        if [ "$CODEBUILD_BUILD_SUCCEEDING" = "1" ]; then
          docker push $ECR_REGISTRY/web:$IMAGE_TAG
          docker push $ECR_REGISTRY/web:latest
          printf '{"ImageURI":"%s"}' "$ECR_REGISTRY/web:$IMAGE_TAG" > imageDetail.json
        fi

artifacts:
  files:
    - imageDetail.json
    - appspec.yml
    - taskdef.json
  name: web-$(date +%Y%m%d-%H%M%S)

reports:
  unit-tests:
    files:
      - reports/junit.xml
    file-format: JUNITXML
  coverage:
    files:
      - coverage/cobertura-coverage.xml
    file-format: COBERTURAXML

cache:
  paths:
    - /root/.pnpm-store/**/*
    - node_modules/**/*

That is the whole production file. Now the block-by-block read.

env.variables — plain text, shows up in the build log. Use for non-secret things like the region and the Node environment.

env.parameter-store — resolves SSM parameters at the start of the build. Encrypted via KMS, never appears in the log. The IAM role attached to the project needs ssm:GetParameters on each parameter ARN. Cheaper than Secrets Manager for tokens that rotate rarely.

env.secrets-manager — resolves Secrets Manager secrets, optionally a specific JSON key with :keyname. Use for credentials that rotate (database passwords, third-party API keys with auto-rotation). The role needs secretsmanager:GetSecretValue.

env.exported-variables — variables to pass to the next CodePipeline stage. Without exporting, the variable is local to the build and lost.

phases.install.runtime-versions — picks the runtime image’s installed Node, Python, etc. Faster than installing your own.

phases.build.on-failure: CONTINUE — the key trick for failed-test visibility. When the test step fails, CodeBuild marks the build as failed, but still runs post_build and uploads the JUnit report. Without CONTINUE, you only get the failure summary; with it, you get the actual list of failing tests in the report group UI.

artifacts.files — files uploaded to the project’s S3 artifact bucket. In CodePipeline, these become the input artifact to the next stage. The imageDetail.json is the convention ECS expects in the deploy stage.

reports — each named report becomes a “report group” in CodeBuild. The UI shows pass/fail counts, trend lines, and which tests are slowest. Reports are free to generate and pnpm test --reporter=junit adds maybe 200ms to the run.

cache.paths — paths preserved between builds when the project’s cache type is LOCAL (kept on the same instance) or S3 (uploaded after the build). pnpm’s store at /root/.pnpm-store is the right cache target for pnpm; for npm, use /root/.npm; for yarn berry, .yarn/cache. Caching node_modules directly is a trap — the cache restore is slower than reinstalling from a warm store on a fast network.

The fix

The mental model that holds across every production buildspec we write:

  1. One repo, one buildspec, multiple projects. Use ${CODEBUILD_BUILD_ID}-derived variables and a single buildspec file for dev, staging, prod. Inject the per-environment variables through the CodeBuild project, not by branching buildspec-prod.yml vs buildspec-staging.yml. The branching version drifts within a quarter.

  2. Resolve secrets only in pre_build. Putting secrets-manager references in env is convenient but means every build pulls every secret, even builds that fail at install. For per-phase secrets (e.g. Sentry token used only by build), pull them in pre_build with aws secretsmanager get-secret-value and export. The IAM permission stays narrow.

  3. Pin runtime versions. nodejs: 20 will silently become 20.x.y across runtime image rebuilds. For deterministic builds, pin in the package.json engines field and assert at install time:

    node --version | grep -q "^v20.11.0" || (echo "Wrong Node version" && exit 1)
  4. Make post_build idempotent. Builds fail. post_build runs even on failed builds when on-failure: CONTINUE is set. Guard pushes with $CODEBUILD_BUILD_SUCCEEDING so a half-built image never lands in ECR.

  5. Use reports, not log scraping. The cheapest debugging upgrade in CodeBuild is wiring the JUnit reporter into the test step and adding the reports block. The next time a test breaks, you click the report and see exactly which test, with the stack trace. No log diving.

  6. Cache the store, not the install. pnpm/yarn/npm install from a warm content-addressable store in under 10 seconds. Restoring a 300 MB node_modules from S3 cache takes 30+ seconds and breaks on Node minor-version changes. Cache the store.

The buildspec grows over time. Every block was added because of a real failure. The discipline is to keep that growth principled — one repo, one file, secrets in pre_build, reports for every test step.


Stack Harbor runs CodeBuild + CodePipeline at production scale as part of Managed AWS onboarding and ongoing Managed Cloud Ops. If your buildspec has drifted into copy-pasted shell with no reports and silent secret failures, start with an infrastructure consult.