CodeBuild: Project, buildspec.yml, and the First Build

K
Kai··5 min read

The code is now on CodeCommit. The next stage of the chain is build: take that source, run the compile/test/package steps, and produce an artifact ready to deploy. This is CodeBuild's job. This article creates a build project, writes a buildspec.yml defining the steps, runs a real build against the app pushed in the previous article, then reads the log to see exactly what CodeBuild does.

Goal

Understand how CodeBuild runs a build (not just "click build"), be able to write a buildspec.yml, create a project via the CLI, and read the result through phases and logs.

buildspec.yml: the build recipe

CodeBuild doesn't guess how to build your app — you declare it in a buildspec.yml file placed at the repo root. The docs describe it as a "collection of build commands and related settings, in YAML format, that CodeBuild uses to run a build." The structure revolves around phases that run sequentially:

version: 0.2

phases:
  install:
    commands:
      - echo "Install phase — preparing the build environment"
  pre_build:
    commands:
      - echo "Pre-build — validating the source"
      - test -f index.html && echo "index.html present"
  build:
    commands:
      - echo "Build phase — static site, packaging as-is"
      - echo "<!-- built by CodeBuild at $(date -u) -->" >> index.html
  post_build:
    commands:
      - echo "Post-build — build finished"

artifacts:
  files:
    - index.html
    - appspec.yml
    - 'scripts/**/*'
  name: awscicd-demo-app

The four phases you declare: install (install tools/runtime), pre_build (preparation, registry login, checks), build (the main build commands), post_build (packaging, pushing images...). Our app is a static page, so the commands are just illustrative; with a real app this is where you call npm run build, mvn package, go build... The artifacts block declares which files get bundled into the output — here index.html plus the appspec.yml and scripts/ that the deploy article will add. Commit this file to the repo and push to CodeCommit (git push origin main).

Choosing the build environment

CodeBuild runs the build inside a prebuilt Docker container. AWS provides "curated" images; don't memorize the names since the list changes — ask the API directly:

$ aws codebuild list-curated-environment-images \
    --query "platforms[].languages[].images[].name" --output text \
    | tr '\t' '\n' | grep amazonlinux-x86_64-standard
aws/codebuild/amazonlinux-x86_64-standard:6.0
...

The latest standard at the time of writing is aws/codebuild/amazonlinux-x86_64-standard:6.0 (based on Amazon Linux 2023). This is the image we use. Tip: in the configuration you can choose "always use the latest version of the runtime" to benefit from AMI caching and shorten provisioning time.

Create the build project

A project ties three things together: source (where to get the code), environment (which image to build in), artifacts (where to push the output), plus the service role (Article 2) for permissions. Declare it with a JSON file for brevity:

{
  "name": "awscicd-demo-build",
  "source": {
    "type": "CODECOMMIT",
    "location": "https://git-codecommit.ap-southeast-1.amazonaws.com/v1/repos/awscicd-demo-app",
    "buildspec": "buildspec.yml"
  },
  "sourceVersion": "refs/heads/main",
  "artifacts": {
    "type": "S3",
    "location": "awscicd-artifacts-111122223333-ap-southeast-1",
    "path": "builds", "packaging": "ZIP", "name": "awscicd-demo-app.zip"
  },
  "environment": {
    "type": "LINUX_CONTAINER",
    "image": "aws/codebuild/amazonlinux-x86_64-standard:6.0",
    "computeType": "BUILD_GENERAL1_SMALL"
  },
  "serviceRole": "arn:aws:iam::111122223333:role/awscicd-codebuild-role"
}
$ aws codebuild create-project --cli-input-json file://cb-project.json \
    --query 'project.[name,environment.image,serviceRole]' --output text
awscicd-demo-build  aws/codebuild/amazonlinux-x86_64-standard:6.0   arn:aws:iam::111122223333:role/awscicd-codebuild-role

The project points its source at CodeCommit, its artifact at the S3 bucket from Article 2, and runs under the CodeBuild service role we created — thanks to that role it has the codecommit:GitPull permission, plus log writing and S3 writing.

Run a real build

$ BUILD_ID=$(aws codebuild start-build --project-name awscicd-demo-build --query 'build.id' --output text)
$ echo "$BUILD_ID"
awscicd-demo-build:67099cde-784b-4788-a31c-dd8004c5cae8

The build runs asynchronously; poll the status until it finishes:

$ aws codebuild batch-get-builds --ids "$BUILD_ID" \
    --query 'builds[0].[buildStatus,currentPhase]' --output text
IN_PROGRESS PROVISIONING
...
SUCCEEDED   COMPLETED

What a build actually involves

This is the part worth dissecting. A build isn't just "run your commands" — CodeBuild goes through a chain of phases, in which the phases you declared (install/pre_build/build/post_build) are only the middle. View them all:

$ aws codebuild batch-get-builds --ids "$BUILD_ID" \
    --query 'builds[0].phases[].[phaseType,phaseStatus,durationInSeconds]' --output text
SUBMITTED         SUCCEEDED  0
QUEUED            SUCCEEDED  0
PROVISIONING      SUCCEEDED  4
DOWNLOAD_SOURCE   SUCCEEDED  5
INSTALL           SUCCEEDED  0
PRE_BUILD         SUCCEEDED  0
BUILD             SUCCEEDED  0
POST_BUILD        SUCCEEDED  0
UPLOAD_ARTIFACTS  SUCCEEDED  0
FINALIZING        SUCCEEDED  0
COMPLETED

PROVISIONING (4s) is when CodeBuild stands up the container from the image; DOWNLOAD_SOURCE (5s) pulls the code from CodeCommit; then come the four phases from buildspec.yml; UPLOAD_ARTIFACTS bundles the output and pushes it to S3; FINALIZING cleans up. This container is ephemeral — stood up for this build, thrown away when done. The next build stands up a fresh container, fully clean, so builds are reproducible: no leftover junk from the previous run.

   CodeCommit (refs/heads/main)
        │  DOWNLOAD_SOURCE
        ▼
   ┌─────── temporary container (amazonlinux-x86_64-standard:6.0) ───────┐
   │   runs buildspec.yml:                                                │
   │   INSTALL → PRE_BUILD → BUILD → POST_BUILD                           │
   └───────┬───────────────────────────────────┬────────────────────────┘
           │ UPLOAD_ARTIFACTS                   │ each command line → log
           ▼                                    ▼
      S3: builds/awscicd-demo-app.zip      CloudWatch Logs
                                           /aws/codebuild/awscicd-demo-build

Reading the log

Each command in the buildspec and its output is written to CloudWatch Logs, group /aws/codebuild/<project>. View a few lines:

$ aws logs get-log-events --log-group-name /aws/codebuild/awscicd-demo-build \
    --log-stream-name 67099cde-... --query 'events[].message' --output text
...
Running command test -f index.html && echo "index.html present"
index.html present
Phase complete: PRE_BUILD State: SUCCEEDED
Entering phase BUILD
Running command echo "Build phase — static site, packaging as-is"
Build phase — static site, packaging as-is
...
Phase complete: POST_BUILD State: SUCCEEDED

The log shows each command run, its output, and the boundary of each phase. When a build fails, this is the first place to look — which phase failed, which command, what error.

The artifact is now on S3, packaged as a ZIP:

$ aws s3 ls s3://awscicd-artifacts-111122223333-ap-southeast-1/builds/ --recursive
2026-05-25 12:53:07        342 builds/awscicd-demo-app.zip

🧹 Cleanup

This project is reused by the pipeline in Part V, so keep it until the end of the series. When you want to delete it:

$ aws codebuild delete-project --name awscicd-demo-build

CodeBuild only bills for actual build minutes, with nothing running in the background, so keeping a project that isn't building costs nothing.

Wrap-up

CodeBuild builds according to the recipe in buildspec.yml: the phases install/pre_build/build/post_build run sequentially inside a temporary container stood up from a curated image (latest amazonlinux-x86_64-standard:6.0). A project ties together source (CodeCommit), environment (image + compute), and artifacts (S3), running under a service role. The build goes through a wider chain of phases than the part you declare (PROVISIONING, DOWNLOAD_SOURCE, UPLOAD_ARTIFACTS...); each command writes to CloudWatch; the artifact comes out as a ZIP on S3. The container is ephemeral, so builds are reproducible.

The next article digs deeper into CodeBuild: passing environment variables and secrets safely (from SSM Parameter Store / Secrets Manager), enabling cache to speed up builds, and tuning the artifacts block.