CodeBuild runs your build in a managed container: it clones the source, runs the commands you declare, and uploads whatever you tell it to keep. You pay per build-minute and nothing when idle.
buildspec.yml, phase by phase
version: 0.2
env:
variables:
NODE_ENV: production
parameter-store:
NPM_TOKEN: /stayhub/prod/npm-token
phases:
install:
runtime-versions:
nodejs: 20
commands:
- npm ci
pre_build:
commands:
- npm run lint
- npm test
build:
commands:
- npm run build
post_build:
commands:
- echo "built $CODEBUILD_RESOLVED_SOURCE_VERSION"
artifacts:
files:
- '**/*'
base-directory: dist
cache:
paths:
- 'node_modules/**/*'The phases are conventions rather than rules — nothing stops you running tests in
build — but keeping to them makes the console's phase timings useful, which is how you
find out that install is two thirds of your build.
One real difference: post_build runs even when build
fails. That is deliberate, so you can publish test reports or clean up, and it is a trap
if you put "push the image" there — it will push after a failed build unless you check
$CODEBUILD_BUILD_SUCCEEDING.
The artifacts block decides what the next stage gets
CodeBuild uploads only what artifacts declares. Omit the block and the next
pipeline stage receives an empty zip — and it fails there, not here, which is why this shows up as
a mysterious deploy failure.
base-directory is the other half. Without it, files: '**/*' uploads
your whole workspace including node_modules, producing an artifact hundreds of
megabytes larger than it needs to be and a slow upload on every build.
Caching, which is usually the whole performance story
By default every build starts from a clean container and downloads every dependency again. On a typical Node or Python project that is most of the build time and most of the bill.
Two kinds of cache, and they are not alternatives:
- Local caching — kept on the build host. Fast, free, and only hits when a
build lands on a host that ran a recent build. Modes include
DOCKER_LAYER_CACHE,SOURCE_CACHEandCUSTOM_CACHE - S3 caching — uploaded and downloaded per build. Reliable, hits every time, and costs the transfer. Better for large dependency trees
aws codebuild update-project --name stayhub-build \
--cache '{"type":"LOCAL","modes":["LOCAL_DOCKER_LAYER_CACHE","LOCAL_SOURCE_CACHE"]}'Docker layer caching is the biggest single win if you build images, and it requires privileged mode — see below. Measure before and after; a cache that never hits is pure overhead.
Compute type: do not guess
Sizes run from BUILD_GENERAL1_SMALL upwards, and a larger one costs more per
minute while finishing sooner. Because you are billed per minute, the bigger instance is sometimes
cheaper overall, and sometimes not — it depends entirely on whether your build
parallelises.
A single-threaded build gains almost nothing from more vCPUs and you pay the higher rate for the same wall-clock time. A test suite that runs in parallel can halve both. Try two sizes on the same commit and compare; it is a ten-minute experiment that settles the question.
Graviton (ARM_CONTAINER) is cheaper per minute again, if your dependencies build
for arm64.
Secrets belong in parameter-store, not variables
Anything under env.variables is plain text in the buildspec, which is in your
repository, and it is echoed in the build log. The parameter-store and
secrets-manager blocks fetch at build time and mask the value in the log.
The project's service role needs ssm:GetParameters and kms:Decrypt
for that to work — and the failure is an unhelpful "parameter not found", which sends people
looking for a typo rather than a permission.
Building a container image
Running docker build inside CodeBuild needs privileged mode, which
is off by default. Without it, the Docker daemon cannot start and the error mentions a socket
rather than a setting.
aws codebuild update-project --name stayhub-build \
--environment '{"type":"LINUX_CONTAINER","image":"aws/codebuild/standard:7.0","computeType":"BUILD_GENERAL1_SMALL","privilegedMode":true}'
aws ecr get-login-password --region us-west-2 \
| docker login --username AWS --password-stdin 111122223333.dkr.ecr.us-west-2.amazonaws.comprivilegedMode is nested inside --environment rather than being a flag
of its own, and --environment replaces the whole block — so you must restate
type, image and computeType or you will reset them to
something you did not choose. This is a general property of the update-project call
and it catches people on --artifacts and --source too.
Note the ECR login is piped rather than passed as an argument, so the token never appears in the process list or the build log.
What a build costs, and the free tier
Billing is per build-minute at a rate set by compute type, with 100 minutes a month free on the smallest Linux size. For a small project that is often the entire CI bill.
The number worth watching is not the rate but the duration, and the duration is usually
dependencies. A build that spends four minutes on npm ci and forty seconds compiling
is a caching problem, not a compute-size problem — and buying a larger instance to fix it makes the
bill worse rather than better.
Debugging a failing build
aws codebuild batch-get-builds --ids stayhub-build:abc-123 \
--query "builds[].[buildStatus,phases[?phaseStatus=='FAILED'].[phaseType,contexts[].message]]"That names the phase that failed, which narrows things immediately: an install
failure is dependencies, pre_build is your tests, and a DOWNLOAD_SOURCE
failure is the connection or the service role rather than anything in your code.
For anything you cannot reproduce from logs, CodeBuild can pause a build and let you connect to
the container with Session Manager — far faster than adding echo statements and
pushing again.